Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc639f0cdc |
+27
-6
@@ -1,3 +1,5 @@
|
||||
const bskyEslint = require('./eslint')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
@@ -23,12 +25,31 @@ module.exports = {
|
||||
'bsky-internal/avoid-unwrapped-text': [
|
||||
'error',
|
||||
{
|
||||
impliedTextComponents: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'],
|
||||
impliedTextProps: [],
|
||||
suggestedTextWrappers: {
|
||||
Button: 'ButtonText',
|
||||
'ToggleButton.Button': 'ToggleButton.ButtonText',
|
||||
},
|
||||
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'],
|
||||
},
|
||||
],
|
||||
'simple-import-sort/imports': [
|
||||
|
||||
@@ -22,9 +22,6 @@ 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 }}
|
||||
|
||||
@@ -47,18 +44,7 @@ jobs:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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"
|
||||
fetch-depth: 100
|
||||
|
||||
- name: ⬇️ Fetch commits from base branch
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
@@ -71,12 +57,12 @@ jobs:
|
||||
if [ -z "${{ inputs.channel == 'production' }}" ]; then
|
||||
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo base-commit=${{ steps.last-successful-commit.base-commit }} >> "$GITHUB_OUTPUT"
|
||||
echo base-commit=$(git log -n 1 --skip 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: ✓ Make sure we found a base commit
|
||||
run: |
|
||||
if [ -z "${{ steps.base-commit.outputs.base-commit }}" && ${{ inputs.channel == 'production' }} ]; then
|
||||
if [ -z "${{ steps.base-commit.outputs.base-commit }}" ]; then
|
||||
echo "Could not find a base commit for this release. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
@@ -96,6 +82,7 @@ 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
|
||||
@@ -103,26 +90,13 @@ jobs:
|
||||
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
|
||||
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
|
||||
|
||||
fingerprintDiff='$(echo "${{ steps.fingerprint.outputs.fingerprint-diff }}")'
|
||||
|
||||
fingerprintDiff="${{ 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'}}
|
||||
@@ -139,6 +113,10 @@ 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: |
|
||||
@@ -158,16 +136,11 @@ 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
|
||||
@@ -233,9 +206,6 @@ 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
|
||||
|
||||
@@ -13,15 +13,12 @@ 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.pull_request.head.repo.full_name == github.repository }}
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -34,6 +31,9 @@ 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,7 +49,6 @@ 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: |
|
||||
@@ -72,7 +71,6 @@ 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
|
||||
|
||||
@@ -84,20 +82,62 @@ jobs:
|
||||
pr_path: '../stats-new.json'
|
||||
excluded_assets: '(.+).chunk.js|(.+).js.map|(.+).json|(.+).png'
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
- name: 🔍 Find old comment if it exists
|
||||
uses: peter-evans/find-comment@v2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
id: old_comment
|
||||
with:
|
||||
header: bundle-diff
|
||||
message: |
|
||||
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 -->
|
||||
| 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.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }}
|
||||
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
|
||||
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
|
||||
@@ -132,28 +172,126 @@ jobs:
|
||||
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
|
||||
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
- 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
|
||||
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' }}
|
||||
with:
|
||||
header: fingerprint-diff
|
||||
message: |
|
||||
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 -->
|
||||
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
|
||||
<details><summary>Fingerprint diff</summary>
|
||||
|
||||
```json
|
||||
${{ steps.fingerprint.outputs.fingerprint-diff }}
|
||||
```
|
||||
\`\`\`json
|
||||
${diff}
|
||||
\`\`\`
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
`;
|
||||
|
||||
- name: 💬 Delete comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }}
|
||||
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
|
||||
with:
|
||||
header: fingerprint-diff
|
||||
delete: true
|
||||
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>
|
||||
|
||||
\`\`\`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,
|
||||
});
|
||||
|
||||
+5
-9
@@ -130,15 +130,11 @@ module.exports = function (config) {
|
||||
// TestFlight builds
|
||||
enabled: IS_TESTFLIGHT,
|
||||
fallbackToCacheTimeout: 30000,
|
||||
codeSigningCertificate: IS_TESTFLIGHT
|
||||
? './code-signing/certificate.pem'
|
||||
: undefined,
|
||||
codeSigningMetadata: IS_TESTFLIGHT
|
||||
? {
|
||||
keyid: 'main',
|
||||
alg: 'rsa-v1_5-sha256',
|
||||
}
|
||||
: undefined,
|
||||
codeSigningCertificate: './code-signing/certificate.pem',
|
||||
codeSigningMetadata: {
|
||||
keyid: 'main',
|
||||
alg: 'rsa-v1_5-sha256',
|
||||
},
|
||||
checkAutomatically: 'NEVER',
|
||||
channel: UPDATES_CHANNEL,
|
||||
},
|
||||
|
||||
@@ -220,21 +220,6 @@
|
||||
.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">
|
||||
|
||||
@@ -199,7 +199,7 @@ describe('avoid-unwrapped-text', () => {
|
||||
|
||||
{
|
||||
code: `
|
||||
<View propText={
|
||||
<View prop={
|
||||
<Trans><Text>foo</Text></Trans>
|
||||
}>
|
||||
<Bar />
|
||||
@@ -246,205 +246,6 @@ 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: [
|
||||
@@ -589,209 +390,6 @@ function MyText({ foo }) {
|
||||
`,
|
||||
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,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -33,14 +33,8 @@ 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)) {
|
||||
@@ -50,18 +44,18 @@ exports.create = function create(context) {
|
||||
while (parent) {
|
||||
if (parent.type === 'JSXElement') {
|
||||
const tagName = getTagName(parent)
|
||||
if (isTextComponent(tagName)) {
|
||||
if (textComponents.includes(tagName) || tagName.endsWith('Text')) {
|
||||
// We're good.
|
||||
return
|
||||
}
|
||||
if (tagName === 'Trans') {
|
||||
// Exit and rely on the traversal for <Trans> JSXElement (code below).
|
||||
// Skip over it and check above.
|
||||
// TODO: Maybe validate that it's present.
|
||||
return
|
||||
parent = parent.parent
|
||||
continue
|
||||
}
|
||||
const suggestedWrapper = suggestedTextWrappers[tagName]
|
||||
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
|
||||
if (tagName !== 'View' && !suggestedWrapper) {
|
||||
let message = 'Wrap this string in <Text>.'
|
||||
if (tagName !== 'View') {
|
||||
message +=
|
||||
' If <' +
|
||||
tagName +
|
||||
@@ -113,219 +107,5 @@ 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>.',
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+28
-20
@@ -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,7 +64,10 @@
|
||||
"@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.23.1",
|
||||
"@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-masked-view/masked-view": "0.3.0",
|
||||
"@react-native-menu/menu": "^0.8.0",
|
||||
"@react-native-picker/picker": "2.6.1",
|
||||
@@ -105,30 +108,27 @@
|
||||
"email-validator": "^2.0.4",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"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": "^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-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.3",
|
||||
"expo-localization": "~14.8.2",
|
||||
"expo-media-library": "~15.9.1",
|
||||
"expo-notifications": "~0.27.6",
|
||||
"expo-notifications": "~0.27.3",
|
||||
"expo-sharing": "^11.10.0",
|
||||
"expo-splash-screen": "~0.26.4",
|
||||
"expo-splash-screen": "~0.26.2",
|
||||
"expo-status-bar": "~1.11.1",
|
||||
"expo-system-ui": "~2.9.3",
|
||||
"expo-task-manager": "~11.7.2",
|
||||
"expo-updates": "~0.24.10",
|
||||
"expo-web-browser": "~12.8.2",
|
||||
"expo-task-manager": "~11.7.0",
|
||||
"expo-updates": "~0.24.7",
|
||||
"expo-web-browser": "~12.8.1",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
"js-sha256": "^0.9.0",
|
||||
@@ -143,6 +143,7 @@
|
||||
"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",
|
||||
@@ -153,16 +154,20 @@
|
||||
"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.11.0",
|
||||
"react-native-get-random-values": "~1.8.0",
|
||||
"react-native-haptic-feedback": "^1.14.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",
|
||||
@@ -174,7 +179,9 @@
|
||||
"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",
|
||||
@@ -183,6 +190,7 @@
|
||||
"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
@@ -16,13 +16,8 @@ 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'
|
||||
@@ -36,6 +31,11 @@ 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 = readLastActiveAccount()
|
||||
const account = persisted.get('session').currentAccount
|
||||
resumeSession(account)
|
||||
}, [resumeSession, _])
|
||||
|
||||
|
||||
+7
-7
@@ -7,13 +7,8 @@ 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'
|
||||
@@ -24,6 +19,11 @@ 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 = readLastActiveAccount()
|
||||
const account = persisted.get('session').currentAccount
|
||||
resumeSession(account)
|
||||
}, [resumeSession])
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Button
|
||||
|
||||
`Button` is intended to be very flexible and can be used to create consistent
|
||||
button styles, as seen in the Storybook, or can be used to create custom
|
||||
buttons with totally independent styles.
|
||||
|
||||
## Usage
|
||||
|
||||
Typical usage looks like this, where the `variant`, `size`, `color`, and/or
|
||||
`shape` are defined. Normal usage also should make use of the `ButtonText` and
|
||||
`ButtonIcon` components so that they inherit styles from the top level props
|
||||
like `color`.
|
||||
|
||||
```tsx
|
||||
<Button
|
||||
label='Typical button'
|
||||
onPress={() => {}}
|
||||
size='large'
|
||||
variant='solid'
|
||||
color='primary'
|
||||
>
|
||||
<ButtonIcon icon={Plus} position='left' />
|
||||
<ButtonText>Hello world</ButtonText>
|
||||
</Button>
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```tsx
|
||||
<Button
|
||||
label='Typical button'
|
||||
onPress={() => {}}
|
||||
size='large'
|
||||
variant='solid'
|
||||
color='primary'
|
||||
>
|
||||
<ButtonText>Hello world</ButtonText>
|
||||
<ButtonIcon icon={Plus} position='right' />
|
||||
</Button>
|
||||
```
|
||||
|
||||
Each of those top-level props props is optional in order to allow for "custom"
|
||||
buttons, like this:
|
||||
|
||||
```tsx
|
||||
<Button label='Custom button' onPress={() => {}}>
|
||||
{({ hovered, focused, pressed, disabled }) => (
|
||||
<View style={[
|
||||
t.atoms.bg_contrast_25,
|
||||
hovered && t.atoms.bg_contrast_50,
|
||||
]}>
|
||||
<Text>{disabled ? 'Disabled' : 'Click me!'}</Text>
|
||||
</View>
|
||||
))}
|
||||
</Button>
|
||||
```
|
||||
|
||||
In the custom button case, you have the _option_ to use `ButtonIcon` and
|
||||
`ButtonText`, but without top-level props like `color`, they serve little
|
||||
purpose, and it may be perferrable to define your own components for these
|
||||
purposes.
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
@@ -58,10 +59,6 @@ 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'
|
||||
@@ -71,9 +68,11 @@ export type ButtonProps = Pick<
|
||||
testID?: string
|
||||
label: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
|
||||
children:
|
||||
| React.ReactNode
|
||||
| string
|
||||
| ((context: ButtonContext) => React.ReactNode | string)
|
||||
}
|
||||
|
||||
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
||||
|
||||
const Context = React.createContext<VariantProps & ButtonState>({
|
||||
@@ -405,7 +404,15 @@ export function Button({
|
||||
</View>
|
||||
)}
|
||||
<Context.Provider value={context}>
|
||||
{typeof children === 'function' ? children(context) : children}
|
||||
{/* @ts-ignore */}
|
||||
{typeof children === 'string' || children?.type === Trans ? (
|
||||
/* @ts-ignore */
|
||||
<ButtonText>{children}</ButtonText>
|
||||
) : typeof children === 'function' ? (
|
||||
children(context)
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Context.Provider>
|
||||
</Pressable>
|
||||
)
|
||||
@@ -39,8 +39,7 @@ export function useDialogControl(): DialogOuterProps['control'] {
|
||||
control.current.open()
|
||||
},
|
||||
close: cb => {
|
||||
control.current.close()
|
||||
cb?.()
|
||||
control.current.close(cb)
|
||||
},
|
||||
}),
|
||||
[id, control],
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import React, {useImperativeHandle} from 'react'
|
||||
import {TouchableWithoutFeedback, View} from 'react-native'
|
||||
import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated'
|
||||
import {View, TouchableWithoutFeedback} from 'react-native'
|
||||
import {FocusScope} from '@tamagui/focus-scope'
|
||||
import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {FocusScope} from '@tamagui/focus-scope'
|
||||
|
||||
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 {useTheme, atoms as a, useBreakpoints, web, flatten} from '#/alf'
|
||||
import {Portal} from '#/components/Portal'
|
||||
|
||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||
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 * from '#/components/Dialog/types'
|
||||
export {Input} from '#/components/forms/TextField'
|
||||
|
||||
@@ -41,31 +37,14 @@ export function Outer({
|
||||
setDialogIsOpen(control.id, true)
|
||||
}, [setIsOpen, setDialogIsOpen, control.id])
|
||||
|
||||
const onCloseInner = React.useCallback(async () => {
|
||||
const close = React.useCallback(async () => {
|
||||
setIsVisible(false)
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
setIsOpen(false)
|
||||
setIsVisible(true)
|
||||
setDialogIsOpen(control.id, false)
|
||||
onClose?.()
|
||||
}, [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],
|
||||
)
|
||||
}, [onClose, setIsOpen, setDialogIsOpen, control.id])
|
||||
|
||||
useImperativeHandle(
|
||||
control.ref,
|
||||
@@ -73,7 +52,7 @@ export function Outer({
|
||||
open,
|
||||
close,
|
||||
}),
|
||||
[close, open],
|
||||
[open, close],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -86,7 +65,7 @@ export function Outer({
|
||||
document.addEventListener('keydown', handler)
|
||||
|
||||
return () => document.removeEventListener('keydown', handler)
|
||||
}, [close, isOpen])
|
||||
}, [isOpen, close])
|
||||
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
@@ -103,7 +82,7 @@ export function Outer({
|
||||
<TouchableWithoutFeedback
|
||||
accessibilityHint={undefined}
|
||||
accessibilityLabel={_(msg`Close active dialog`)}
|
||||
onPress={onCloseInner}>
|
||||
onPress={close}>
|
||||
<View
|
||||
style={[
|
||||
web(a.fixed),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
|
||||
import {atoms as a, tokens} from '#/alf'
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ export type InlineLinkProps = React.PropsWithChildren<
|
||||
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
|
||||
>
|
||||
|
||||
export function InlineLinkText({
|
||||
export function InlineLink({
|
||||
children,
|
||||
to,
|
||||
action = 'push',
|
||||
|
||||
@@ -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, ButtonText} from '#/components/Button'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Error} from '#/components/Error'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -87,9 +87,7 @@ function ListFooterMaybeError({
|
||||
a.py_sm,
|
||||
]}
|
||||
onPress={onRetry}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react'
|
||||
import Animated, {
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withRepeat,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
||||
import {atoms as a, useTheme, flatten} from '#/alf'
|
||||
import {Props, useCommonSVGProps} from '#/components/icons/common'
|
||||
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
+18
-13
@@ -51,7 +51,7 @@ export function Outer({
|
||||
)
|
||||
}
|
||||
|
||||
export function TitleText({children}: React.PropsWithChildren<{}>) {
|
||||
export function Title({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 TitleText({children}: React.PropsWithChildren<{}>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DescriptionText({children}: React.PropsWithChildren<{}>) {
|
||||
export function Description({children}: React.PropsWithChildren<{}>) {
|
||||
const t = useTheme()
|
||||
const {descriptionId} = React.useContext(Context)
|
||||
return (
|
||||
@@ -91,13 +91,15 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
|
||||
}
|
||||
|
||||
export function Cancel({
|
||||
children,
|
||||
cta,
|
||||
}: {
|
||||
}: React.PropsWithChildren<{
|
||||
/**
|
||||
* Optional i18n string. If undefined, it will default to "Cancel".
|
||||
* Optional i18n string, used in lieu of `children` for simple buttons. If
|
||||
* undefined (and `children` is undefined), it will default to "Cancel".
|
||||
*/
|
||||
cta?: string
|
||||
}) {
|
||||
}>) {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {close} = Dialog.useDialogContext()
|
||||
@@ -112,30 +114,33 @@ export function Cancel({
|
||||
size={gtMobile ? 'small' : 'medium'}
|
||||
label={cta || _(msg`Cancel`)}
|
||||
onPress={onPress}>
|
||||
<ButtonText>{cta || _(msg`Cancel`)}</ButtonText>
|
||||
{children ? children : <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function Action({
|
||||
children,
|
||||
onPress,
|
||||
color = 'primary',
|
||||
cta,
|
||||
testID,
|
||||
}: {
|
||||
}: React.PropsWithChildren<{
|
||||
onPress: () => void
|
||||
color?: ButtonColor
|
||||
/**
|
||||
* Optional i18n string. If undefined, it will default to "Confirm".
|
||||
* Optional i18n string, used in lieu of `children` for simple buttons. If
|
||||
* undefined (and `children` is 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 (
|
||||
@@ -146,7 +151,7 @@ export function Action({
|
||||
label={cta || _(msg`Confirm`)}
|
||||
onPress={handleOnPress}
|
||||
testID={testID}>
|
||||
<ButtonText>{cta || _(msg`Confirm`)}</ButtonText>
|
||||
{children ? children : <ButtonText>{cta || _(msg`Confirm`)}</ButtonText>}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -170,8 +175,8 @@ export function Basic({
|
||||
}>) {
|
||||
return (
|
||||
<Outer control={control} testID="confirmModal">
|
||||
<TitleText>{title}</TitleText>
|
||||
<DescriptionText>{description}</DescriptionText>
|
||||
<Title>{title}</Title>
|
||||
<Description>{description}</Description>
|
||||
<Actions>
|
||||
<Action
|
||||
cta={confirmButtonCta}
|
||||
|
||||
@@ -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 {InlineLinkText} from '#/components/Link'
|
||||
import {InlineLink} 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(
|
||||
<InlineLinkText
|
||||
<InlineLink
|
||||
selectable={selectable}
|
||||
key={key}
|
||||
to={`/profile/${mention.did}`}
|
||||
@@ -92,14 +92,14 @@ export function RichText({
|
||||
// @ts-ignore TODO
|
||||
dataSet={WORD_WRAP}>
|
||||
{segment.text}
|
||||
</InlineLinkText>,
|
||||
</InlineLink>,
|
||||
)
|
||||
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
|
||||
if (disableLinks) {
|
||||
els.push(toShortUrl(segment.text))
|
||||
} else {
|
||||
els.push(
|
||||
<InlineLinkText
|
||||
<InlineLink
|
||||
selectable={selectable}
|
||||
key={key}
|
||||
to={link.uri}
|
||||
@@ -108,7 +108,7 @@ export function RichText({
|
||||
dataSet={WORD_WRAP}
|
||||
shareOnLongPress>
|
||||
{toShortUrl(segment.text)}
|
||||
</InlineLinkText>,
|
||||
</InlineLink>,
|
||||
)
|
||||
}
|
||||
} else if (
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +1,37 @@
|
||||
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 {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
useRemoveMutedWordMutation,
|
||||
useUpsertMutedWordsMutation,
|
||||
useRemoveMutedWordMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {
|
||||
atoms as a,
|
||||
native,
|
||||
useBreakpoints,
|
||||
useTheme,
|
||||
useBreakpoints,
|
||||
ViewStyleProp,
|
||||
web,
|
||||
native,
|
||||
} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
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 {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
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 {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 {Text} from '#/components/Typography'
|
||||
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
|
||||
export function MutedWordsDialog() {
|
||||
const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
|
||||
@@ -129,9 +130,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
||||
<TargetToggle>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText>
|
||||
<Toggle.Label>
|
||||
<Trans>Mute in text & tags</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Label>
|
||||
</View>
|
||||
<PageText size="sm" />
|
||||
</TargetToggle>
|
||||
@@ -144,9 +145,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
||||
<TargetToggle>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText>
|
||||
<Toggle.Label>
|
||||
<Trans>Mute in tags only</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Label>
|
||||
</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 LabelText = TextField.LabelText
|
||||
export const Label = TextField.Label
|
||||
|
||||
export function DateField({
|
||||
value,
|
||||
|
||||
@@ -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 LabelText = TextField.LabelText
|
||||
export const Label = TextField.Label
|
||||
|
||||
/**
|
||||
* Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date
|
||||
|
||||
@@ -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 LabelText = TextField.LabelText
|
||||
export const Label = TextField.Label
|
||||
|
||||
const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>(
|
||||
({style, ...props}, ref) => {
|
||||
|
||||
@@ -225,7 +225,7 @@ export function createInput(Component: typeof TextInput) {
|
||||
|
||||
export const Input = createInput(TextInput)
|
||||
|
||||
export function LabelText({
|
||||
export function Label({
|
||||
nativeID,
|
||||
children,
|
||||
}: React.PropsWithChildren<{nativeID?: string}>) {
|
||||
@@ -288,7 +288,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
|
||||
)
|
||||
}
|
||||
|
||||
export function SuffixText({
|
||||
export function Suffix({
|
||||
children,
|
||||
label,
|
||||
accessibilityHint,
|
||||
|
||||
@@ -3,16 +3,16 @@ import {Pressable, View, ViewStyle} from 'react-native'
|
||||
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {
|
||||
atoms as a,
|
||||
flatten,
|
||||
native,
|
||||
TextStyleProp,
|
||||
useTheme,
|
||||
atoms as a,
|
||||
native,
|
||||
flatten,
|
||||
ViewStyleProp,
|
||||
TextStyleProp,
|
||||
} 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 LabelText({
|
||||
export function Label({
|
||||
children,
|
||||
style,
|
||||
}: React.PropsWithChildren<TextStyleProp>) {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react'
|
||||
import {AccessibilityProps, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import {View, AccessibilityProps, TextStyle, ViewStyle} from 'react-native'
|
||||
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {atoms as a, useTheme, native} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
|
||||
AccessibilityProps & {
|
||||
children: React.ReactElement
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
|
||||
export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
|
||||
AccessibilityProps &
|
||||
React.PropsWithChildren<{
|
||||
testID?: string
|
||||
}
|
||||
}>
|
||||
|
||||
export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & {
|
||||
multiple?: boolean
|
||||
@@ -46,42 +47,49 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
|
||||
const t = useTheme()
|
||||
const state = Toggle.useItemContext()
|
||||
|
||||
const {baseStyles, hoverStyles, activeStyles} = React.useMemo(() => {
|
||||
const base: ViewStyle[] = []
|
||||
const hover: ViewStyle[] = []
|
||||
const active: ViewStyle[] = []
|
||||
const {baseStyles, hoverStyles, activeStyles, textStyles} =
|
||||
React.useMemo(() => {
|
||||
const base: ViewStyle[] = []
|
||||
const hover: ViewStyle[] = []
|
||||
const active: ViewStyle[] = []
|
||||
const text: TextStyle[] = []
|
||||
|
||||
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,
|
||||
})
|
||||
hover.push({
|
||||
backgroundColor: t.palette.contrast_800,
|
||||
})
|
||||
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.disabled) {
|
||||
active.push({
|
||||
backgroundColor: t.palette.contrast_500,
|
||||
base.push({
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
})
|
||||
text.push({
|
||||
opacity: 0.5,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (state.disabled) {
|
||||
base.push({
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
baseStyles: base,
|
||||
hoverStyles: hover,
|
||||
activeStyles: active,
|
||||
}
|
||||
}, [t, state])
|
||||
return {
|
||||
baseStyles: base,
|
||||
hoverStyles: hover,
|
||||
activeStyles: active,
|
||||
textStyles: text,
|
||||
}
|
||||
}, [t, state])
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -102,37 +110,19 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
|
||||
activeStyles,
|
||||
(state.hovered || state.pressed) && hoverStyles,
|
||||
]}>
|
||||
{children}
|
||||
{typeof children === 'string' ? (
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
textStyles,
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
) : (
|
||||
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 {InlineLinkText} from '#/components/Link'
|
||||
import {InlineLink} 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}>
|
||||
<ToggleButton.ButtonText>{ignoreLabel}</ToggleButton.ButtonText>
|
||||
{ignoreLabel}
|
||||
</ToggleButton.Button>
|
||||
)}
|
||||
{warnLabel && (
|
||||
<ToggleButton.Button name="warn" label={warnLabel}>
|
||||
<ToggleButton.ButtonText>{warnLabel}</ToggleButton.ButtonText>
|
||||
{warnLabel}
|
||||
</ToggleButton.Button>
|
||||
)}
|
||||
{hideLabel && (
|
||||
<ToggleButton.Button name="hide" label={hideLabel}>
|
||||
<ToggleButton.ButtonText>{hideLabel}</ToggleButton.ButtonText>
|
||||
{hideLabel}
|
||||
</ToggleButton.Button>
|
||||
)}
|
||||
</ToggleButton.Group>
|
||||
@@ -243,9 +243,9 @@ export function LabelerLabelPreference({
|
||||
) : isGlobalLabel ? (
|
||||
<Trans>
|
||||
Configured in{' '}
|
||||
<InlineLinkText to="/moderation" style={a.text_sm}>
|
||||
<InlineLink to="/moderation" style={a.text_sm}>
|
||||
moderation settings
|
||||
</InlineLinkText>
|
||||
</InlineLink>
|
||||
.
|
||||
</Trans>
|
||||
) : null}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
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 * 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 * 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 {Divider} from '../Divider'
|
||||
|
||||
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
|
||||
@@ -144,13 +145,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>{' '}
|
||||
<InlineLinkText
|
||||
<InlineLink
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}>
|
||||
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
|
||||
</InlineLinkText>
|
||||
</InlineLink>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -203,14 +204,14 @@ function AppealForm({
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
This appeal will be sent to{' '}
|
||||
<InlineLinkText
|
||||
<InlineLink
|
||||
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}
|
||||
</InlineLinkText>
|
||||
</InlineLink>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
@@ -244,7 +245,7 @@ function AppealForm({
|
||||
size="medium"
|
||||
onPress={onPressBack}
|
||||
label={_(msg`Back`)}>
|
||||
<ButtonText>{_(msg`Back`)}</ButtonText>
|
||||
{_(msg`Back`)}
|
||||
</Button>
|
||||
<Button
|
||||
testID="submitBtn"
|
||||
@@ -253,7 +254,7 @@ function AppealForm({
|
||||
size="medium"
|
||||
onPress={onSubmit}
|
||||
label={_(msg`Submit`)}>
|
||||
<ButtonText>{_(msg`Submit`)}</ButtonText>
|
||||
{_(msg`Submit`)}
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
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 {atoms as a, useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {useTheme, atoms as a} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {InlineLink} from '#/components/Link'
|
||||
import {Divider} from '#/components/Divider'
|
||||
|
||||
export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog'
|
||||
|
||||
@@ -54,9 +55,9 @@ function ModerationDetailsDialogInner({
|
||||
description = (
|
||||
<Trans>
|
||||
This user is included in the{' '}
|
||||
<InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}>
|
||||
<InlineLink to={listUriToHref(list.uri)} style={[a.text_sm]}>
|
||||
{list.name}
|
||||
</InlineLinkText>{' '}
|
||||
</InlineLink>{' '}
|
||||
list which you have blocked.
|
||||
</Trans>
|
||||
)
|
||||
@@ -83,9 +84,9 @@ function ModerationDetailsDialogInner({
|
||||
description = (
|
||||
<Trans>
|
||||
This user is included in the{' '}
|
||||
<InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}>
|
||||
<InlineLink to={listUriToHref(list.uri)} style={[a.text_sm]}>
|
||||
{list.name}
|
||||
</InlineLinkText>{' '}
|
||||
</InlineLink>{' '}
|
||||
list which you have muted.
|
||||
</Trans>
|
||||
)
|
||||
@@ -126,12 +127,12 @@ function ModerationDetailsDialogInner({
|
||||
{modcause.source.type === 'user' ? (
|
||||
<Trans>the author</Trans>
|
||||
) : (
|
||||
<InlineLinkText
|
||||
<InlineLink
|
||||
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
||||
onPress={() => control.close()}
|
||||
style={a.text_md}>
|
||||
{desc.source}
|
||||
</InlineLinkText>
|
||||
</InlineLink>
|
||||
)}
|
||||
.
|
||||
</Trans>
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
|
||||
import VersionNumber from 'react-native-version-number'
|
||||
|
||||
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 = `${nativeApplicationVersion} (${nativeBuildVersion}, ${
|
||||
IS_DEV ? 'development' : UPDATES_CHANNEL
|
||||
})`
|
||||
export const appVersion = `${VersionNumber.appVersion} (${
|
||||
VersionNumber.buildVersion
|
||||
}, ${IS_DEV ? 'development' : UPDATES_CHANNEL})`
|
||||
|
||||
@@ -4,7 +4,6 @@ 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}`
|
||||
@@ -81,10 +80,8 @@ 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 = [
|
||||
BSKY_APP_ACCOUNT_DID,
|
||||
'did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||
'did:plc:vpkhqolt662uhesyj6nxm7ys',
|
||||
'did:plc:q6gjnaw2blty4crticxkmujt',
|
||||
]
|
||||
|
||||
+11
-18
@@ -1,35 +1,28 @@
|
||||
import {
|
||||
impactAsync,
|
||||
ImpactFeedbackStyle,
|
||||
notificationAsync,
|
||||
NotificationFeedbackType,
|
||||
selectionAsync,
|
||||
} from 'expo-haptics'
|
||||
|
||||
import {isIOS, isWeb} from 'platform/detection'
|
||||
import ReactNativeHapticFeedback, {
|
||||
HapticFeedbackTypes,
|
||||
} from 'react-native-haptic-feedback'
|
||||
|
||||
const hapticImpact: ImpactFeedbackStyle = isIOS
|
||||
? ImpactFeedbackStyle.Medium
|
||||
: ImpactFeedbackStyle.Light // Users said the medium impact was too strong on Android; see APP-537s
|
||||
const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537s
|
||||
|
||||
export class Haptics {
|
||||
static default() {
|
||||
if (isWeb) {
|
||||
return
|
||||
}
|
||||
impactAsync(hapticImpact)
|
||||
ReactNativeHapticFeedback.trigger(hapticImpact)
|
||||
}
|
||||
static impact(type: ImpactFeedbackStyle = hapticImpact) {
|
||||
static impact(type: HapticFeedbackTypes = hapticImpact) {
|
||||
if (isWeb) {
|
||||
return
|
||||
}
|
||||
impactAsync(type)
|
||||
ReactNativeHapticFeedback.trigger(type)
|
||||
}
|
||||
static selection() {
|
||||
if (isWeb) {
|
||||
return
|
||||
}
|
||||
selectionAsync()
|
||||
ReactNativeHapticFeedback.trigger('selection')
|
||||
}
|
||||
static notification = (type: 'success' | 'warning' | 'error') => {
|
||||
if (isWeb) {
|
||||
@@ -37,11 +30,11 @@ export class Haptics {
|
||||
}
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return notificationAsync(NotificationFeedbackType.Success)
|
||||
return ReactNativeHapticFeedback.trigger('notificationSuccess')
|
||||
case 'warning':
|
||||
return notificationAsync(NotificationFeedbackType.Warning)
|
||||
return ReactNativeHapticFeedback.trigger('notificationWarning')
|
||||
case 'error':
|
||||
return notificationAsync(NotificationFeedbackType.Error)
|
||||
return ReactNativeHapticFeedback.trigger('notificationError')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import {useCallback} from 'react'
|
||||
|
||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||
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 {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 {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {LogEvents} from '../statsig/statsig'
|
||||
|
||||
export function useAccountSwitcher() {
|
||||
@@ -45,14 +44,9 @@ export function useAccountSwitcher() {
|
||||
'circle-exclamation',
|
||||
)
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(`switch account: selectAccount failed`, {
|
||||
message: e.message,
|
||||
})
|
||||
} catch (e) {
|
||||
Toast.show('Sorry! We need you to enter your password.')
|
||||
clearCurrentAccount() // back user out to login
|
||||
setTimeout(() => {
|
||||
Toast.show('Sorry! We need you to enter your password.')
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
[
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {Alert, AppState, AppStateStatus} from 'react-native'
|
||||
import {nativeBuildVersion} from 'expo-application'
|
||||
import app from 'react-native-version-number'
|
||||
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
|
||||
`${nativeBuildVersion}`,
|
||||
`${app.buildVersion}`,
|
||||
)
|
||||
await setExtraParamAsync(
|
||||
'channel',
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import {Platform} from 'react-native'
|
||||
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
|
||||
import app from 'react-native-version-number'
|
||||
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 = nativeApplicationVersion ?? 'dev'
|
||||
const release = app.appVersion ?? 'dev'
|
||||
|
||||
/**
|
||||
* Examples:
|
||||
@@ -33,7 +33,7 @@ const release = nativeApplicationVersion ?? 'dev'
|
||||
* - `android.1.57.0.46`
|
||||
*/
|
||||
const dist = `${Platform.OS}.${release}${
|
||||
nativeBuildVersion ? `.${nativeBuildVersion}` : ''
|
||||
app.buildVersion ? `.${app.buildVersion}` : ''
|
||||
}`
|
||||
|
||||
init({
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import {Share} from 'react-native'
|
||||
// import * as Sharing from 'expo-sharing'
|
||||
import {setStringAsync} from 'expo-clipboard'
|
||||
import Clipboard from '@react-native-clipboard/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
|
||||
setStringAsync(url)
|
||||
Clipboard.setString(url)
|
||||
Toast.show('Copied to clipboard')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,10 @@ 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'
|
||||
}
|
||||
@@ -67,10 +65,6 @@ 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': {
|
||||
@@ -83,9 +77,6 @@ export type LogEvents = {
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
}
|
||||
'profile:follow': {
|
||||
didBecomeMutual: boolean | undefined
|
||||
followeeClout: number | undefined
|
||||
followerClout: number | undefined
|
||||
logContext:
|
||||
| 'RecommendedFollowsItem'
|
||||
| 'PostThreadItem'
|
||||
|
||||
@@ -43,14 +43,6 @@ 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,
|
||||
@@ -86,10 +78,6 @@ function toStatsigUser(did: string | undefined) {
|
||||
return {
|
||||
userID,
|
||||
platform: Platform.OS,
|
||||
custom: {
|
||||
// Need to specify here too for gating.
|
||||
platform: Platform.OS,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,14 +19,6 @@ 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
|
||||
|
||||
@@ -5,13 +5,12 @@ 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, ButtonText} from '#/components/Button'
|
||||
import {Button} from '#/components/Button'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {FormContainer} from './FormContainer'
|
||||
|
||||
@@ -39,22 +38,15 @@ export const ChooseAccountForm = ({
|
||||
setShowLoggedOut(false)
|
||||
Toast.show(_(msg`Already signed in as @${account.handle}`))
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
onSelectAccount(account)
|
||||
@@ -66,11 +58,11 @@ export const ChooseAccountForm = ({
|
||||
return (
|
||||
<FormContainer
|
||||
testID="chooseAccountForm"
|
||||
titleText={<Trans>Select account</Trans>}>
|
||||
title={<Trans>Select account</Trans>}>
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<TextField.Label>
|
||||
<Trans>Sign in as...</Trans>
|
||||
</TextField.LabelText>
|
||||
</TextField.Label>
|
||||
<AccountList
|
||||
onSelectAccount={onSelect}
|
||||
onSelectOther={() => onSelectAccount()}
|
||||
@@ -83,7 +75,7 @@ export const ChooseAccountForm = ({
|
||||
color="secondary"
|
||||
size="medium"
|
||||
onPress={onPressBack}>
|
||||
<ButtonText>{_(msg`Back`)}</ButtonText>
|
||||
{_(msg`Back`)}
|
||||
</Button>
|
||||
<View style={[a.flex_1]} />
|
||||
</View>
|
||||
|
||||
@@ -83,11 +83,11 @@ export const ForgotPasswordForm = ({
|
||||
return (
|
||||
<FormContainer
|
||||
testID="forgotPasswordForm"
|
||||
titleText={<Trans>Reset password</Trans>}>
|
||||
title={<Trans>Reset password</Trans>}>
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<TextField.Label>
|
||||
<Trans>Hosting provider</Trans>
|
||||
</TextField.LabelText>
|
||||
</TextField.Label>
|
||||
<HostingProvider
|
||||
serviceUrl={serviceUrl}
|
||||
onSelectServiceUrl={setServiceUrl}
|
||||
@@ -95,9 +95,9 @@ export const ForgotPasswordForm = ({
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<TextField.Label>
|
||||
<Trans>Email address</Trans>
|
||||
</TextField.LabelText>
|
||||
</TextField.Label>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={At} />
|
||||
<TextField.Input
|
||||
|
||||
@@ -6,12 +6,12 @@ import {Text} from '#/components/Typography'
|
||||
|
||||
export function FormContainer({
|
||||
testID,
|
||||
titleText,
|
||||
title,
|
||||
children,
|
||||
style,
|
||||
}: {
|
||||
testID?: string
|
||||
titleText?: React.ReactNode
|
||||
title?: 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]}>
|
||||
{titleText && !gtMobile && (
|
||||
{title && !gtMobile && (
|
||||
<Text style={[a.text_xl, a.font_bold, t.atoms.text_contrast_high]}>
|
||||
{titleText}
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{children}
|
||||
|
||||
@@ -128,11 +128,11 @@ export const LoginForm = ({
|
||||
|
||||
const isReady = !!serviceDescription && !!identifier && !!password
|
||||
return (
|
||||
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
|
||||
<FormContainer testID="loginForm" title={<Trans>Sign in</Trans>}>
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<TextField.Label>
|
||||
<Trans>Hosting provider</Trans>
|
||||
</TextField.LabelText>
|
||||
</TextField.Label>
|
||||
<HostingProvider
|
||||
serviceUrl={serviceUrl}
|
||||
onSelectServiceUrl={setServiceUrl}
|
||||
@@ -140,9 +140,9 @@ export const LoginForm = ({
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<TextField.Label>
|
||||
<Trans>Account</Trans>
|
||||
</TextField.LabelText>
|
||||
</TextField.Label>
|
||||
<View style={[a.gap_sm]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={At} />
|
||||
@@ -237,9 +237,7 @@ export const LoginForm = ({
|
||||
color="secondary"
|
||||
size="medium"
|
||||
onPress={onPressRetryConnect}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
{_(msg`Retry`)}
|
||||
</Button>
|
||||
) : !serviceDescription ? (
|
||||
<>
|
||||
|
||||
@@ -99,7 +99,7 @@ export const SetNewPasswordForm = ({
|
||||
return (
|
||||
<FormContainer
|
||||
testID="setNewPasswordForm"
|
||||
titleText={<Trans>Set new password</Trans>}>
|
||||
title={<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.LabelText>Reset code</TextField.LabelText>
|
||||
<TextField.Label>Reset code</TextField.Label>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Ticket} />
|
||||
<TextField.Input
|
||||
@@ -131,7 +131,7 @@ export const SetNewPasswordForm = ({
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<TextField.LabelText>New password</TextField.LabelText>
|
||||
<TextField.Label>New password</TextField.Label>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Lock} />
|
||||
<TextField.Input
|
||||
|
||||
@@ -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 {InlineLinkText, Link} from '#/components/Link'
|
||||
import {InlineLink, 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.LabelText style={[a.text_md, a.flex_1]}>
|
||||
<Toggle.Label style={[a.text_md, a.flex_1]}>
|
||||
<Trans>
|
||||
Discourage apps from showing my account to logged-out users
|
||||
</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
|
||||
{updateProfile.isPending && <Loader />}
|
||||
@@ -545,9 +545,9 @@ function PwiOptOut() {
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<InlineLinkText to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
|
||||
<InlineLink to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
|
||||
<Trans>Learn more about what is public on Bluesky.</Trans>
|
||||
</InlineLinkText>
|
||||
</InlineLink>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
import {IS_DEV} from '#/env'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {ScrollView} from '#/view/com/util/Views'
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
|
||||
import {
|
||||
atoms as a,
|
||||
flatten,
|
||||
native,
|
||||
TextStyleProp,
|
||||
useBreakpoints,
|
||||
useTheme,
|
||||
atoms as a,
|
||||
useBreakpoints,
|
||||
web,
|
||||
native,
|
||||
flatten,
|
||||
TextStyleProp,
|
||||
} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
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 {leading, P, Text} from '#/components/Typography'
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
|
||||
const COL_WIDTH = 500
|
||||
|
||||
@@ -73,7 +75,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
onPress={() => onboardDispatch({type: 'skip'})}
|
||||
// DEV ONLY
|
||||
label="Clear onboarding state">
|
||||
<ButtonText>Clear</ButtonText>
|
||||
Clear
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
@@ -202,7 +204,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function TitleText({
|
||||
export function Title({
|
||||
children,
|
||||
style,
|
||||
}: React.PropsWithChildren<TextStyleProp>) {
|
||||
@@ -222,7 +224,7 @@ export function TitleText({
|
||||
)
|
||||
}
|
||||
|
||||
export function DescriptionText({
|
||||
export function Description({
|
||||
children,
|
||||
style,
|
||||
}: React.PropsWithChildren<TextStyleProp>) {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {Image} from 'expo-image'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||
import {FeedConfig} from '#/screens/Onboarding/StepAlgoFeeds'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useTheme, atoms as a} 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 {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 {FeedConfig} from '#/screens/Onboarding/StepAlgoFeeds'
|
||||
|
||||
function PrimaryFeedCardInner({
|
||||
feed,
|
||||
|
||||
@@ -6,9 +6,9 @@ import {useLingui} from '@lingui/react'
|
||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {
|
||||
DescriptionText,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
TitleText,
|
||||
Title,
|
||||
} from '#/screens/Onboarding/Layout'
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
|
||||
@@ -34,6 +34,11 @@ 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[] = [
|
||||
@@ -100,15 +105,15 @@ export function StepAlgoFeeds() {
|
||||
<View style={[a.align_start]}>
|
||||
<IconCircle icon={ListSparkle} style={[a.mb_2xl]} />
|
||||
|
||||
<TitleText>
|
||||
<Title>
|
||||
<Trans>Choose your main feeds</Trans>
|
||||
</TitleText>
|
||||
<DescriptionText>
|
||||
</Title>
|
||||
<Description>
|
||||
<Trans>
|
||||
Custom feeds built by the community bring you new experiences and help
|
||||
you find the content you love.
|
||||
</Trans>
|
||||
</DescriptionText>
|
||||
</Description>
|
||||
|
||||
<View style={[a.w_full, a.pb_2xl]}>
|
||||
<Toggle.Group
|
||||
@@ -125,6 +130,16 @@ 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
|
||||
|
||||
@@ -4,16 +4,15 @@ 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 {
|
||||
DescriptionText,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
TitleText,
|
||||
Title,
|
||||
} from '#/screens/Onboarding/Layout'
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
import {
|
||||
@@ -56,9 +55,7 @@ export function StepFinished() {
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
bulkWriteFollows(
|
||||
suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID),
|
||||
),
|
||||
bulkWriteFollows(suggestedAccountsStepResults.accountDids),
|
||||
// these must be serial
|
||||
(async () => {
|
||||
await getAgent().setInterestsPref({tags: selectedInterests})
|
||||
@@ -90,12 +87,12 @@ export function StepFinished() {
|
||||
<View style={[a.align_start]}>
|
||||
<IconCircle icon={Check} style={[a.mb_2xl]} />
|
||||
|
||||
<TitleText>
|
||||
<Title>
|
||||
<Trans>You're ready to go!</Trans>
|
||||
</TitleText>
|
||||
<DescriptionText>
|
||||
</Title>
|
||||
<Description>
|
||||
<Trans>We hope you have a wonderful time. Remember, Bluesky is:</Trans>
|
||||
</DescriptionText>
|
||||
</Description>
|
||||
|
||||
<View style={[a.pt_5xl, a.gap_3xl]}>
|
||||
<View style={[a.flex_row, a.align_center, a.w_full, a.gap_lg]}>
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
useSetFeedViewPreferencesMutation,
|
||||
} from 'state/queries/preferences'
|
||||
import {
|
||||
DescriptionText,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
TitleText,
|
||||
Title,
|
||||
} 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]} />
|
||||
|
||||
<TitleText>
|
||||
<Title>
|
||||
<Trans>Your default feed is "Following"</Trans>
|
||||
</TitleText>
|
||||
<DescriptionText style={[a.mb_md]}>
|
||||
</Title>
|
||||
<Description style={[a.mb_md]}>
|
||||
<Trans>It shows posts from the people you follow as they happen.</Trans>
|
||||
</DescriptionText>
|
||||
</Description>
|
||||
|
||||
<View style={[a.w_full]}>
|
||||
<Toggle.Item
|
||||
@@ -139,9 +139,9 @@ export function StepFollowingFeed() {
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
|
||||
<DescriptionText style={[a.mt_lg]}>
|
||||
<Description style={[a.mt_lg]}>
|
||||
<Trans>You can change these settings later.</Trans>
|
||||
</DescriptionText>
|
||||
</Description>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
<Button
|
||||
|
||||
@@ -11,9 +11,9 @@ import {logger} from '#/logger'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {
|
||||
DescriptionText,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
TitleText,
|
||||
Title,
|
||||
} 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() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<TitleText>{title}</TitleText>
|
||||
<DescriptionText>{description}</DescriptionText>
|
||||
<Title>{title}</Title>
|
||||
<Description>{description}</Description>
|
||||
|
||||
<View style={[a.w_full, a.pt_2xl]}>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -113,15 +113,15 @@ export function AdultContentEnabledPref({
|
||||
)}
|
||||
|
||||
<Prompt.Outer control={prompt}>
|
||||
<Prompt.TitleText>
|
||||
<Prompt.Title>
|
||||
<Trans>Adult Content</Trans>
|
||||
</Prompt.TitleText>
|
||||
<Prompt.DescriptionText>
|
||||
</Prompt.Title>
|
||||
<Prompt.Description>
|
||||
<Trans>
|
||||
Due to Apple policies, adult content can only be enabled on the web
|
||||
after completing sign up.
|
||||
</Trans>
|
||||
</Prompt.DescriptionText>
|
||||
</Prompt.Description>
|
||||
<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 {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {LabelPreference, InterpretedLabelValueDefinition} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
|
||||
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
usePreferencesSetContentLabelMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
|
||||
|
||||
export function ModerationOption({
|
||||
labelValueDefinition,
|
||||
@@ -83,13 +83,13 @@ export function ModerationOption({
|
||||
values={[visibility ?? 'hide']}
|
||||
onChange={onChange}>
|
||||
<ToggleButton.Button name="ignore" label={labels.show}>
|
||||
<ToggleButton.ButtonText>{labels.show}</ToggleButton.ButtonText>
|
||||
{labels.show}
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="warn" label={labels.warn}>
|
||||
<ToggleButton.ButtonText>{labels.warn}</ToggleButton.ButtonText>
|
||||
{labels.warn}
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="hide" label={labels.hide}>
|
||||
<ToggleButton.ButtonText>{labels.hide}</ToggleButton.ButtonText>
|
||||
{labels.hide}
|
||||
</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 {
|
||||
DescriptionText,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
TitleText,
|
||||
Title,
|
||||
} 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]} />
|
||||
|
||||
<TitleText>
|
||||
<Title>
|
||||
<Trans>You're in control</Trans>
|
||||
</TitleText>
|
||||
<DescriptionText style={[a.mb_xl]}>
|
||||
</Title>
|
||||
<Description style={[a.mb_xl]}>
|
||||
<Trans>
|
||||
Select what you want to see (or not see), and we’ll handle the rest.
|
||||
</Trans>
|
||||
</DescriptionText>
|
||||
</Description>
|
||||
|
||||
{!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 {
|
||||
DescriptionText,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
TitleText,
|
||||
Title,
|
||||
} 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]} />
|
||||
|
||||
<TitleText>
|
||||
<Title>
|
||||
<Trans>Here are some accounts for you to follow</Trans>
|
||||
</TitleText>
|
||||
<DescriptionText>
|
||||
</Title>
|
||||
<Description>
|
||||
{state.interestsStepResults.selectedInterests.length ? (
|
||||
<Trans>Based on your interest in {interestsText}</Trans>
|
||||
) : (
|
||||
<Trans>These are popular accounts you might like:</Trans>
|
||||
)}
|
||||
</DescriptionText>
|
||||
</Description>
|
||||
|
||||
<View style={[a.w_full, a.pt_xl]}>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -9,9 +9,9 @@ import {capitalize} from '#/lib/strings/capitalize'
|
||||
import {IS_TEST_USER} from 'lib/constants'
|
||||
import {useSession} from 'state/session'
|
||||
import {
|
||||
DescriptionText,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
TitleText,
|
||||
Title,
|
||||
} 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]} />
|
||||
|
||||
<TitleText>
|
||||
<Title>
|
||||
<Trans>Feeds can be topical as well!</Trans>
|
||||
</TitleText>
|
||||
<DescriptionText>
|
||||
</Title>
|
||||
<Description>
|
||||
{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>
|
||||
)}
|
||||
</DescriptionText>
|
||||
</Description>
|
||||
|
||||
<View style={[a.w_full, a.pb_2xl, a.pt_2xl]}>
|
||||
<Toggle.Group
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
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,
|
||||
@@ -27,7 +28,7 @@ export function ProfileHeaderMetrics({
|
||||
<View
|
||||
style={[a.flex_row, a.gap_sm, a.align_center, a.pb_md]}
|
||||
pointerEvents="box-none">
|
||||
<InlineLinkText
|
||||
<InlineLink
|
||||
testID="profileHeaderFollowersButton"
|
||||
style={[a.flex_row, t.atoms.text]}
|
||||
to={makeProfileLink(profile, 'followers')}
|
||||
@@ -36,8 +37,8 @@ export function ProfileHeaderMetrics({
|
||||
<Text style={[t.atoms.text_contrast_medium, a.text_md]}>
|
||||
{pluralizedFollowers}
|
||||
</Text>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText
|
||||
</InlineLink>
|
||||
<InlineLink
|
||||
testID="profileHeaderFollowsButton"
|
||||
style={[a.flex_row, t.atoms.text]}
|
||||
to={makeProfileLink(profile, 'follows')}
|
||||
@@ -48,7 +49,7 @@ export function ProfileHeaderMetrics({
|
||||
following
|
||||
</Text>
|
||||
</Trans>
|
||||
</InlineLinkText>
|
||||
</InlineLink>
|
||||
<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.TitleText>Unable to subscribe</Prompt.TitleText>
|
||||
<Prompt.DescriptionText>
|
||||
<Prompt.Title>Unable to subscribe</Prompt.Title>
|
||||
<Prompt.Description>
|
||||
<Trans>
|
||||
We're sorry! You can only subscribe to ten labelers, and you've
|
||||
reached your limit of ten.
|
||||
</Trans>
|
||||
</Prompt.DescriptionText>
|
||||
</Prompt.Description>
|
||||
<Prompt.Actions>
|
||||
<Prompt.Action onPress={control.close} cta={_(msg`OK`)} />
|
||||
</Prompt.Actions>
|
||||
|
||||
@@ -10,9 +10,7 @@ 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 {
|
||||
@@ -80,9 +78,6 @@ let ProfileHeaderStandard = ({
|
||||
})
|
||||
}, [track, openModal, profile])
|
||||
|
||||
const autoExpandSuggestionsOnProfileFollow = useGate(
|
||||
'autoexpand_suggestions_on_profile_follow',
|
||||
)
|
||||
const onPressFollow = () => {
|
||||
requireAuth(async () => {
|
||||
try {
|
||||
@@ -96,9 +91,6 @@ let ProfileHeaderStandard = ({
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
if (isWeb && autoExpandSuggestionsOnProfileFollow) {
|
||||
setShowSuggestedFollows(true)
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to follow', {message: String(e)})
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import React, {memo} from 'react'
|
||||
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
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 {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 {atoms as a, useTheme} from '#/alf'
|
||||
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
|
||||
import {BlurView} from 'view/com/util/BlurView'
|
||||
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 {
|
||||
@@ -96,7 +97,9 @@ let ProfileHeaderShell = ({
|
||||
accessibilityLabel={_(msg`Back`)}
|
||||
accessibilityHint="">
|
||||
<View style={styles.backBtnWrapper}>
|
||||
<FontAwesomeIcon size={18} icon="angle-left" color="white" />
|
||||
<BlurView style={styles.backBtn} blurType="dark">
|
||||
<FontAwesomeIcon size={18} icon="angle-left" color="white" />
|
||||
</BlurView>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
|
||||
@@ -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 {InlineLinkText} from '#/components/Link'
|
||||
import {InlineLink} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export const Policies = ({
|
||||
@@ -45,16 +45,16 @@ export const Policies = ({
|
||||
const els = []
|
||||
if (tos) {
|
||||
els.push(
|
||||
<InlineLinkText key="tos" to={tos}>
|
||||
<InlineLink key="tos" to={tos}>
|
||||
{_(msg`Terms of Service`)}
|
||||
</InlineLinkText>,
|
||||
</InlineLink>,
|
||||
)
|
||||
}
|
||||
if (pp) {
|
||||
els.push(
|
||||
<InlineLinkText key="pp" to={pp}>
|
||||
<InlineLink key="pp" to={pp}>
|
||||
{_(msg`Privacy Policy`)}
|
||||
</InlineLinkText>,
|
||||
</InlineLink>,
|
||||
)
|
||||
}
|
||||
if (els.length === 2) {
|
||||
|
||||
@@ -36,9 +36,9 @@ export function StepInfo() {
|
||||
<View style={[a.gap_md]}>
|
||||
<FormError error={state.error} />
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<TextField.Label>
|
||||
<Trans>Hosting provider</Trans>
|
||||
</TextField.LabelText>
|
||||
</TextField.Label>
|
||||
<HostingProvider
|
||||
serviceUrl={state.serviceUrl}
|
||||
onSelectServiceUrl={v =>
|
||||
@@ -54,9 +54,9 @@ export function StepInfo() {
|
||||
<>
|
||||
{state.serviceDescription.inviteCodeRequired && (
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<TextField.Label>
|
||||
<Trans>Invite code</Trans>
|
||||
</TextField.LabelText>
|
||||
</TextField.Label>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Ticket} />
|
||||
<TextField.Input
|
||||
@@ -76,9 +76,9 @@ export function StepInfo() {
|
||||
</View>
|
||||
)}
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<TextField.Label>
|
||||
<Trans>Email</Trans>
|
||||
</TextField.LabelText>
|
||||
</TextField.Label>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Envelope} />
|
||||
<TextField.Input
|
||||
@@ -97,9 +97,9 @@ export function StepInfo() {
|
||||
</TextField.Root>
|
||||
</View>
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<TextField.Label>
|
||||
<Trans>Password</Trans>
|
||||
</TextField.LabelText>
|
||||
</TextField.Label>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Lock} />
|
||||
<TextField.Input
|
||||
@@ -117,9 +117,9 @@ export function StepInfo() {
|
||||
</TextField.Root>
|
||||
</View>
|
||||
<View>
|
||||
<DateField.LabelText>
|
||||
<DateField.Label>
|
||||
<Trans>Your birth date</Trans>
|
||||
</DateField.LabelText>
|
||||
</DateField.Label>
|
||||
<DateField.DateField
|
||||
testID="date"
|
||||
value={DateField.utils.toSimpleDateString(state.dateOfBirth)}
|
||||
|
||||
@@ -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 {InlineLinkText} from '#/components/Link'
|
||||
import {InlineLink} 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>{' '}
|
||||
<InlineLinkText to={FEEDBACK_FORM_URL({email: state.email})}>
|
||||
<InlineLink to={FEEDBACK_FORM_URL({email: state.email})}>
|
||||
<Trans>Contact support</Trans>
|
||||
</InlineLinkText>
|
||||
</InlineLink>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -3,6 +3,7 @@ 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'
|
||||
@@ -124,6 +125,12 @@ 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
|
||||
@@ -162,6 +169,7 @@ export type Modal =
|
||||
|
||||
// Generic
|
||||
| LinkWarningModal
|
||||
| EmbedConsentModal
|
||||
| InAppBrowserConsentModal
|
||||
|
||||
const ModalContext = React.createContext<{
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
|
||||
import BroadcastChannel from '#/lib/broadcast'
|
||||
import {logger} from '#/logger'
|
||||
import {migrate} from '#/state/persisted/legacy'
|
||||
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 {PersistedAccount, Schema} from '#/state/persisted/schema'
|
||||
export type {Schema, PersistedAccount} from '#/state/persisted/schema'
|
||||
export {defaults} from '#/state/persisted/schema'
|
||||
|
||||
const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL')
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import {z} from 'zod'
|
||||
|
||||
import {deviceLocales} from '#/platform/detection'
|
||||
|
||||
const externalEmbedOptions = ['show', 'hide'] as const
|
||||
|
||||
/**
|
||||
* A account persisted to storage. Stored in the `accounts[]` array. Contains
|
||||
* base account info and access tokens.
|
||||
*/
|
||||
// only data needed for rendering account page
|
||||
const accountSchema = z.object({
|
||||
service: z.string(),
|
||||
did: z.string(),
|
||||
@@ -20,25 +16,12 @@ 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: currentAccountScheme.optional(),
|
||||
currentAccount: accountSchema.optional(),
|
||||
}),
|
||||
reminders: z.object({
|
||||
lastEmailConfirm: z.string().optional(),
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {BskyAgent} from '@atproto/api'
|
||||
|
||||
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
|
||||
|
||||
export const PUBLIC_BSKY_AGENT = new BskyAgent({
|
||||
service: PUBLIC_BSKY_SERVICE,
|
||||
service: 'https://public.api.bsky.app',
|
||||
})
|
||||
|
||||
export const STALE = {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import {useCallback} from 'react'
|
||||
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {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, toClout} from '#/lib/statsig/statsig'
|
||||
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
|
||||
import {updatePostShadow} from '#/state/cache/post-shadow'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {findProfileQueryData} from './profile'
|
||||
import {getAgent} from '#/state/session'
|
||||
|
||||
const RQKEY_ROOT = 'post'
|
||||
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
|
||||
@@ -69,7 +68,7 @@ export function usePostLikeMutationQueue(
|
||||
const postUri = post.uri
|
||||
const postCid = post.cid
|
||||
const initialLikeUri = post.viewer?.like
|
||||
const likeMutation = usePostLikeMutation(logContext, post)
|
||||
const likeMutation = usePostLikeMutation(logContext)
|
||||
const unlikeMutation = usePostUnlikeMutation(logContext)
|
||||
|
||||
const queueToggle = useToggleMutationQueue({
|
||||
@@ -118,40 +117,15 @@ export function usePostLikeMutationQueue(
|
||||
return [queueLike, queueUnlike]
|
||||
}
|
||||
|
||||
function usePostLikeMutation(
|
||||
logContext: LogEvents['post:like']['logContext'],
|
||||
post: Shadow<AppBskyFeedDefs.PostView>,
|
||||
) {
|
||||
const {currentAccount} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
const postAuthor = post.author
|
||||
function usePostLikeMutation(logContext: LogEvents['post:like']['logContext']) {
|
||||
return useMutation<
|
||||
{uri: string}, // responds with the uri of the like
|
||||
Error,
|
||||
{uri: string; cid: string} // the post's uri and 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)
|
||||
mutationFn: post => {
|
||||
logEvent('post:like', {logContext})
|
||||
return getAgent().like(post.uri, post.cid)
|
||||
},
|
||||
onSuccess() {
|
||||
track('Post:Like')
|
||||
|
||||
@@ -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, toClout} from '#/lib/statsig/statsig'
|
||||
import {logEvent, LogEvents} 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, profile)
|
||||
const followMutation = useProfileFollowMutation(logContext)
|
||||
const unfollowMutation = useProfileUnfollowMutation(logContext)
|
||||
|
||||
const queueToggle = useToggleMutationQueue({
|
||||
@@ -252,24 +252,10 @@ 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}) => {
|
||||
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),
|
||||
})
|
||||
logEvent('profile:follow', {logContext})
|
||||
return await getAgent().follow(did)
|
||||
},
|
||||
onSuccess(data, variables) {
|
||||
@@ -544,12 +530,3 @@ export function* findAllProfilesInQueryData(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function findProfileQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
): AppBskyActorDefs.ProfileViewDetailed | undefined {
|
||||
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewDetailed>(
|
||||
RQKEY(did),
|
||||
)
|
||||
}
|
||||
|
||||
+492
-329
File diff suppressed because it is too large
Load Diff
@@ -1,88 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
|
||||
type StateContext = string
|
||||
type SetContext = (v: string) => void
|
||||
@@ -10,7 +8,7 @@ type SetContext = (v: string) => void
|
||||
const stateContext = React.createContext<StateContext>('home')
|
||||
const setContext = React.createContext<SetContext>((_: string) => {})
|
||||
|
||||
function getInitialFeed(startSessionWithFollowing: boolean) {
|
||||
function getInitialFeed() {
|
||||
if (isWeb) {
|
||||
if (window.location.pathname === '/') {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
@@ -26,21 +24,16 @@ function getInitialFeed(startSessionWithFollowing: boolean) {
|
||||
return feedFromSession
|
||||
}
|
||||
}
|
||||
if (!startSessionWithFollowing) {
|
||||
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
|
||||
if (feedFromPersisted) {
|
||||
// Fall back to the last chosen one across all tabs.
|
||||
return feedFromPersisted
|
||||
}
|
||||
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 startSessionWithFollowing = useGate('start_session_with_following')
|
||||
const [state, setState] = React.useState(() =>
|
||||
getInitialFeed(startSessionWithFollowing),
|
||||
)
|
||||
const [state, setState] = React.useState(getInitialFeed)
|
||||
|
||||
const saveState = React.useCallback((feed: string) => {
|
||||
setState(feed)
|
||||
|
||||
@@ -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 {InlineLinkText} from '#/components/Link'
|
||||
import {InlineLink} 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,
|
||||
]}>
|
||||
<InlineLinkText to="https://bsky.social">
|
||||
<InlineLink to="https://bsky.social">
|
||||
<Trans>Business</Trans>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText to="https://bsky.social/about/blog">
|
||||
</InlineLink>
|
||||
<InlineLink to="https://bsky.social/about/blog">
|
||||
<Trans>Blog</Trans>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText to="https://bsky.social/about/join">
|
||||
</InlineLink>
|
||||
<InlineLink to="https://bsky.social/about/join">
|
||||
<Trans>Jobs</Trans>
|
||||
</InlineLinkText>
|
||||
</InlineLink>
|
||||
|
||||
<View style={a.flex_1} />
|
||||
|
||||
|
||||
@@ -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 * as persisted from '#/state/persisted'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {BSKY_SERVICE} from 'lib/constants'
|
||||
import * as persisted from '#/state/persisted'
|
||||
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {Text, P} from '#/components/Typography'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||
import {P, Text} from '#/components/Typography'
|
||||
|
||||
export function ServerInputDialog({
|
||||
control,
|
||||
@@ -87,17 +87,13 @@ export function ServerInputDialog({
|
||||
values={fixedOption}
|
||||
onChange={setFixedOption}>
|
||||
<ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}>
|
||||
<ToggleButton.ButtonText>
|
||||
{_(msg`Bluesky`)}
|
||||
</ToggleButton.ButtonText>
|
||||
{_(msg`Bluesky`)}
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button
|
||||
testID="customSelectBtn"
|
||||
name="custom"
|
||||
label={_(msg`Custom`)}>
|
||||
<ToggleButton.ButtonText>
|
||||
{_(msg`Custom`)}
|
||||
</ToggleButton.ButtonText>
|
||||
{_(msg`Custom`)}
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
|
||||
@@ -110,9 +106,9 @@ export function ServerInputDialog({
|
||||
a.px_md,
|
||||
a.py_md,
|
||||
]}>
|
||||
<TextField.LabelText nativeID="address-input-label">
|
||||
<TextField.Label nativeID="address-input-label">
|
||||
<Trans>Server address</Trans>
|
||||
</TextField.LabelText>
|
||||
</TextField.Label>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Globe} />
|
||||
<Dialog.Input
|
||||
@@ -167,7 +163,7 @@ export function ServerInputDialog({
|
||||
size="small"
|
||||
onPress={() => control.close()}
|
||||
label={_(msg`Done`)}>
|
||||
<ButtonText>{_(msg`Done`)}</ButtonText>
|
||||
{_(msg`Done`)}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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,7 +508,11 @@ export const ComposePost = observer(function ComposePost({
|
||||
title={_(msg`Discard draft?`)}
|
||||
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
||||
onConfirm={() => {
|
||||
discardPromptControl.close(onClose)
|
||||
if (isWeb) {
|
||||
onClose()
|
||||
} else {
|
||||
discardPromptControl.close(onClose)
|
||||
}
|
||||
}}
|
||||
confirmButtonCta={_(msg`Discard`)}
|
||||
confirmButtonColor="negative"
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import React from 'react'
|
||||
import {useWindowDimensions, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
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 {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 {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 {ListMethods} from '../util/List'
|
||||
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
|
||||
import {MainScrollProvider} from '../util/MainScrollProvider'
|
||||
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 {isNative} from '#/platform/detection'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
|
||||
const POLL_FREQ = 60e3 // 60sec
|
||||
|
||||
@@ -72,7 +71,6 @@ export function FeedPage({
|
||||
setHasNew(false)
|
||||
logEvent('feed:refresh', {
|
||||
feedType: feed.split('|')[0],
|
||||
feedUrl: feed,
|
||||
reason: 'soft-reset',
|
||||
})
|
||||
}
|
||||
@@ -98,22 +96,10 @@ 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>
|
||||
@@ -122,7 +108,7 @@ export function FeedPage({
|
||||
enabled={isPageFocused}
|
||||
feed={feed}
|
||||
feedParams={feedParams}
|
||||
pollInterval={feedPollInterval}
|
||||
pollInterval={POLL_FREQ}
|
||||
disablePoll={hasNew}
|
||||
scrollElRef={scrollElRef}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import React, {useState} from 'react'
|
||||
import {StyleSheet, TextInput, TouchableOpacity, View} from 'react-native'
|
||||
import {setStringAsync} from 'expo-clipboard'
|
||||
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 {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
useAppPasswordCreateMutation,
|
||||
useAppPasswordsQuery,
|
||||
useAppPasswordCreateMutation,
|
||||
} 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%']
|
||||
|
||||
@@ -73,7 +72,7 @@ export function Component({}: {}) {
|
||||
|
||||
const onCopy = React.useCallback(() => {
|
||||
if (appPassword) {
|
||||
setStringAsync(appPassword)
|
||||
Clipboard.setString(appPassword)
|
||||
Toast.show(_(msg`Copied to clipboard`))
|
||||
setWasCopied(true)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import React, {useCallback, useMemo, useState} from 'react'
|
||||
import React, {useMemo, useCallback, useState} from 'react'
|
||||
import {
|
||||
ImageStyle,
|
||||
ScrollView as RNScrollView,
|
||||
StyleSheet,
|
||||
TextInput as RNTextInput,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
TextInput as RNTextInput,
|
||||
useWindowDimensions,
|
||||
ScrollView as RNScrollView,
|
||||
} from 'react-native'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
import {Image} from 'expo-image'
|
||||
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 {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {enforceLen} from 'lib/strings/helpers'
|
||||
import {gradients, s} from 'lib/styles'
|
||||
import {enforceLen} from 'lib/strings/helpers'
|
||||
import {MAX_ALT_TEXT} from 'lib/constants'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
|
||||
import {Text} from '../util/text/Text'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {ImageModel} from 'state/models/media/image'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['100%']
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import React, {useState} from 'react'
|
||||
import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {getAgent, useSession, useSessionApi} from '#/state/session'
|
||||
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 {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
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'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useSession, useSessionApi, getAgent} from '#/state/session'
|
||||
|
||||
enum Stages {
|
||||
InputEmail,
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
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 {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 {
|
||||
getAgent,
|
||||
SessionAccount,
|
||||
useSession,
|
||||
useSessionApi,
|
||||
} 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 {ScrollView, TextInput} from './util'
|
||||
import {Text} from '../util/text/Text'
|
||||
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'
|
||||
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 {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useServiceQuery} from '#/state/queries/service'
|
||||
import {useUpdateHandleMutation, useFetchDid} from '#/state/queries/handle'
|
||||
import {
|
||||
useSession,
|
||||
useSessionApi,
|
||||
SessionAccount,
|
||||
getAgent,
|
||||
} from '#/state/session'
|
||||
|
||||
export const snapPoints = ['100%']
|
||||
|
||||
@@ -322,7 +321,9 @@ function CustomHandleForm({
|
||||
// events
|
||||
// =
|
||||
const onPressCopy = React.useCallback(() => {
|
||||
setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did)
|
||||
Clipboard.setString(
|
||||
isDNSForm ? `did=${currentAccount.did}` : currentAccount.did,
|
||||
)
|
||||
Toast.show(_(msg`Copied to clipboard`))
|
||||
}, [currentAccount, isDNSForm, _])
|
||||
const onChangeHandle = React.useCallback(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback, useMemo, useState} from 'react'
|
||||
import React, {useState, useCallback, useMemo} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
@@ -8,36 +8,35 @@ 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 {msg, Trans} from '@lingui/macro'
|
||||
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 {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
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {Pressable, StyleSheet, View} from 'react-native'
|
||||
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 {useWindowDimensions} from 'react-native'
|
||||
import {gradients, s} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {getKeys} from 'lib/type-assertions'
|
||||
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 {GalleryModel} from 'state/models/media/gallery'
|
||||
import {ImageModel} from 'state/models/media/image'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {TextInput} from './util'
|
||||
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'
|
||||
|
||||
export const snapPoints = ['80%']
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import React, {useState, useCallback} from 'react'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
@@ -8,30 +9,28 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import Animated, {FadeOut} from 'react-native-reanimated'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
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 {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useProfileUpdateMutation} from '#/state/queries/profile'
|
||||
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'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
const AnimatedTouchableOpacity =
|
||||
Animated.createAnimatedComponent(TouchableOpacity)
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
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,
|
||||
},
|
||||
})
|
||||
@@ -1,37 +1,36 @@
|
||||
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 {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 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 {UserInfoText} from '../util/UserInfoText'
|
||||
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 {useLingui} from '@lingui/react'
|
||||
|
||||
export const snapPoints = ['70%']
|
||||
|
||||
@@ -149,7 +148,7 @@ function InviteCode({
|
||||
const uses = invite.uses
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
setStringAsync(invite.code)
|
||||
Clipboard.setString(invite.code)
|
||||
Toast.show(_(msg`Copied to clipboard`))
|
||||
setInviteCopied(invite.code)
|
||||
}, [setInviteCopied, invite, _])
|
||||
|
||||
@@ -15,6 +15,7 @@ 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'
|
||||
@@ -115,6 +116,9 @@ 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} />
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||
import {TouchableWithoutFeedback, StyleSheet, 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 * 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 CropImageModal from './crop-image/CropImage.web'
|
||||
import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as EditImageModal from './EditImage'
|
||||
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 InviteCodesModal from './InviteCodes'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as LinkWarningModal from './LinkWarning'
|
||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
||||
import * as ListAddUserModal from './ListAddRemoveUsers'
|
||||
import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as RepostModal from './Repost'
|
||||
import * as SelfLabelModal from './SelfLabel'
|
||||
import * as ThreadgateModal from './Threadgate'
|
||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
||||
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 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()
|
||||
@@ -111,6 +112,8 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
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 LinearGradient from 'react-native-linear-gradient'
|
||||
import {s, colors, gradients} from 'lib/styles'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {RepostIcon} from 'lib/icons'
|
||||
import {colors, gradients, s} from 'lib/styles'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = [250]
|
||||
|
||||
|
||||
@@ -6,24 +6,23 @@ import {
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {Circle, Path, Svg} from 'react-native-svg'
|
||||
import {Svg, Circle, Path} from 'react-native-svg'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
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 {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 {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
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'
|
||||
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'
|
||||
|
||||
export const snapPoints = ['90%']
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
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 ImageEditor from 'react-avatar-editor'
|
||||
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {RectTallIcon, RectWideIcon, SquareIcon} from 'lib/icons'
|
||||
import {Slider} from '@miblanchard/react-native-slider'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {Dimensions} from 'lib/media/types'
|
||||
import {getDataUriSize} from 'lib/media/util'
|
||||
import {gradients, s} from 'lib/styles'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
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'
|
||||
|
||||
enum AspectRatio {
|
||||
Square = 'square',
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react'
|
||||
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 {StyleSheet, Text, View, Pressable} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {s, colors, gradients} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {colors, gradients, s} from 'lib/styles'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const ConfirmLanguagesButton = ({
|
||||
onPress,
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
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 {useNavigation} from '@react-navigation/native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {track} from 'lib/analytics/analytics'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {s} from 'lib/styles'
|
||||
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})
|
||||
@@ -48,10 +47,8 @@ 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.
|
||||
@@ -139,15 +136,7 @@ function PostThreadFollowBtnLoaded({
|
||||
type="button"
|
||||
style={[!isFollowing ? palInverted.text : pal.text, s.bold]}
|
||||
numberOfLines={1}>
|
||||
{!isFollowing ? (
|
||||
showFollowBackLabel && isFollowedBy ? (
|
||||
<Trans>Follow Back</Trans>
|
||||
) : (
|
||||
<Trans>Follow</Trans>
|
||||
)
|
||||
) : (
|
||||
<Trans>Following</Trans>
|
||||
)}
|
||||
{!isFollowing ? <Trans>Follow</Trans> : <Trans>Following</Trans>}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
+22
-26
@@ -8,33 +8,32 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
|
||||
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'
|
||||
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 {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
|
||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
|
||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||
@@ -218,7 +217,6 @@ let Feed = ({
|
||||
track('Feed:onRefresh')
|
||||
logEvent('feed:refresh', {
|
||||
feedType: feedType,
|
||||
feedUrl: feed,
|
||||
reason: 'pull-to-refresh',
|
||||
})
|
||||
setIsPTRing(true)
|
||||
@@ -229,14 +227,13 @@ let Feed = ({
|
||||
logger.error('Failed to refresh posts feed', {message: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, track, setIsPTRing, onHasNew, feed, feedType])
|
||||
}, [refetch, track, setIsPTRing, onHasNew, feedType])
|
||||
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
|
||||
logEvent('feed:endReached', {
|
||||
feedType: feedType,
|
||||
feedUrl: feed,
|
||||
itemCount: feedItems.length,
|
||||
})
|
||||
track('Feed:onEndReached')
|
||||
@@ -251,7 +248,6 @@ let Feed = ({
|
||||
isError,
|
||||
fetchNextPage,
|
||||
track,
|
||||
feed,
|
||||
feedType,
|
||||
feedItems.length,
|
||||
])
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export {BlurView} from '@react-native-community/blur'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user