Compare commits

..

1 Commits

Author SHA1 Message Date
Eric Bailey fc639f0cdc Button docs 2024-04-04 15:27:58 -05:00
118 changed files with 2346 additions and 2932 deletions
+27 -6
View File
@@ -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': [
+9 -39
View File
@@ -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
+164 -26
View File
@@ -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
View File
@@ -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,
},
-15
View File
@@ -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">
+1 -403
View File
@@ -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,
},
],
}
+6 -226
View File
@@ -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
View File
@@ -35,7 +35,7 @@
"e2e:run": "NODE_ENV=test detox test --configuration ios.sim.debug --take-screenshots all",
"perf:test": "NODE_ENV=test maestro test",
"perf:test:run": "NODE_ENV=test maestro test __e2e__/maestro/scroll.yaml",
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand \"yarn perf:test\" --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure",
"intl:build": "yarn intl:extract && yarn intl:compile",
@@ -64,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
View File
@@ -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
View File
@@ -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])
+61
View File
@@ -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>
)
+1 -2
View File
@@ -39,8 +39,7 @@ export function useDialogControl(): DialogOuterProps['control'] {
control.current.open()
},
close: cb => {
control.current.close()
cb?.()
control.current.close(cb)
},
}),
[id, control],
+16 -37
View File
@@ -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 -1
View File
@@ -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'
+1 -1
View File
@@ -250,7 +250,7 @@ export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
>
export function InlineLinkText({
export function InlineLink({
children,
to,
action = 'push',
+2 -4
View File
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
import {cleanError} from 'lib/strings/errors'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, 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>
+2 -2
View File
@@ -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'
-34
View File
@@ -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
View File
@@ -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}
+5 -5
View File
@@ -7,7 +7,7 @@ import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {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 (
-119
View File
@@ -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>
)
}
+18 -17
View File
@@ -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,
+1 -1
View File
@@ -13,7 +13,7 @@ import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared'
export * as utils from '#/components/forms/DateField/utils'
export const LabelText = TextField.LabelText
export const Label = TextField.Label
/**
* Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date
+1 -1
View File
@@ -9,7 +9,7 @@ import * as TextField from '#/components/forms/TextField'
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
export * as utils from '#/components/forms/DateField/utils'
export const LabelText = TextField.LabelText
export const Label = TextField.Label
const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>(
({style, ...props}, ref) => {
+2 -2
View File
@@ -225,7 +225,7 @@ export function createInput(Component: typeof TextInput) {
export const Input = createInput(TextInput)
export function 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,
+6 -6
View File
@@ -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>) {
+57 -67
View File
@@ -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}
+12 -11
View File
@@ -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
View File
@@ -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})`
+1 -4
View File
@@ -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
View File
@@ -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')
}
}
}
+6 -12
View File
@@ -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)
}
},
[
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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')
}
}
-9
View File
@@ -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'
-12
View File
@@ -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,
},
}
}
-8
View File
@@ -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
+14 -22
View File
@@ -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>
+5 -5
View File
@@ -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
+4 -4
View File
@@ -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}
+6 -8
View File
@@ -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 ? (
<>
+3 -3
View File
@@ -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
+5 -5
View File
@@ -40,7 +40,7 @@ import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filte
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import * as LabelingService from '#/components/LabelingServiceCard'
import {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>
)
+16 -14
View File
@@ -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,
+21 -6
View File
@@ -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
+7 -10
View File
@@ -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]}>
+8 -8
View File
@@ -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 well 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 ? (
+6 -6
View File
@@ -9,9 +9,9 @@ import {capitalize} from '#/lib/strings/capitalize'
import {IS_TEST_USER} from 'lib/constants'
import {useSession} from 'state/session'
import {
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
+8 -7
View File
@@ -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)})
+14 -11
View File
@@ -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>
)}
+5 -5
View File
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {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) {
+10 -10
View File
@@ -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)}
+3 -3
View File
@@ -24,7 +24,7 @@ import {StepInfo} from '#/screens/Signup/StepInfo'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {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>
+8
View File
@@ -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<{
+3 -4
View File
@@ -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')
+2 -19
View File
@@ -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 -3
View File
@@ -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 = {
+8 -34
View File
@@ -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')
+3 -26
View File
@@ -20,7 +20,7 @@ import {track} from '#/lib/analytics/analytics'
import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {logEvent, LogEvents, 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),
)
}
File diff suppressed because it is too large Load Diff
-88
View File
@@ -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
}
-179
View File
@@ -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,
}
}
+7 -14
View File
@@ -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)
+7 -7
View File
@@ -14,7 +14,7 @@ import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {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} />
+11 -15
View File
@@ -1,17 +1,17 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * 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>
+6 -2
View File
@@ -11,8 +11,8 @@ import {
TouchableOpacity,
View,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {LinearGradient} from 'expo-linear-gradient'
import {RichText} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
@@ -508,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"
+17 -31
View File
@@ -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}
+12 -13
View File
@@ -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)
}
+13 -14
View File
@@ -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%']
+11 -12
View File
@@ -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,
+27 -26
View File
@@ -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(
+18 -19
View File
@@ -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
+1 -1
View File
@@ -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'
+16 -17
View File
@@ -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%']
+19 -20
View File
@@ -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)
+153
View File
@@ -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,
},
})
+20 -21
View File
@@ -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, _])
+4
View File
@@ -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} />
+22 -19
View File
@@ -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
}
+7 -8
View File
@@ -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]
+13 -14
View File
@@ -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
View File
@@ -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,
])
+30
View File
@@ -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',
},
})
+1
View File
@@ -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