Merge branch 'main' into hailey/oauth
# Conflicts: # src/screens/Login/LoginForm.tsx
This commit is contained in:
+5
-13
@@ -23,20 +23,12 @@ module.exports = {
|
||||
'bsky-internal/avoid-unwrapped-text': [
|
||||
'error',
|
||||
{
|
||||
impliedTextComponents: [
|
||||
'Button', // TODO: Not always safe.
|
||||
'H1',
|
||||
'H2',
|
||||
'H3',
|
||||
'H4',
|
||||
'H5',
|
||||
'H6',
|
||||
'P',
|
||||
'Prompt.Cancel', // TODO: Not always safe.
|
||||
'Prompt.Action', // TODO: Not always safe.
|
||||
'ToggleButton.Button', // TODO: Not always safe.
|
||||
],
|
||||
impliedTextComponents: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'],
|
||||
impliedTextProps: [],
|
||||
suggestedTextWrappers: {
|
||||
Button: 'ButtonText',
|
||||
'ToggleButton.Button': 'ToggleButton.ButtonText',
|
||||
},
|
||||
},
|
||||
],
|
||||
'simple-import-sort/imports': [
|
||||
|
||||
@@ -22,6 +22,9 @@ jobs:
|
||||
bundleDeploy:
|
||||
name: Bundle and Deploy EAS Update
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
|
||||
cancel-in-progress: true
|
||||
outputs:
|
||||
fingerprint-is-different: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different }}
|
||||
|
||||
@@ -44,7 +47,18 @@ jobs:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 100
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⬇️ Get last successful deployment commit from the cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: last-successful-commit-hash.txt
|
||||
key: last-successful-deployment-commit-${{ github.ref_name }}
|
||||
|
||||
- name: Add the last successful deployment commit to the output
|
||||
id: last-successful-commit
|
||||
run: echo base-commit=$(cat last-successful-commit-hash.txt) >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: ⬇️ Fetch commits from base branch
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
@@ -57,12 +71,12 @@ jobs:
|
||||
if [ -z "${{ inputs.channel == 'production' }}" ]; then
|
||||
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo base-commit=$(git log -n 1 --skip 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
|
||||
echo base-commit=${{ steps.last-successful-commit.base-commit }} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: ✓ Make sure we found a base commit
|
||||
run: |
|
||||
if [ -z "${{ steps.base-commit.outputs.base-commit }}" ]; then
|
||||
if [ -z "${{ steps.base-commit.outputs.base-commit }}" && ${{ inputs.channel == 'production' }} ]; then
|
||||
echo "Could not find a base commit for this release. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
@@ -82,7 +96,6 @@ jobs:
|
||||
uses: expo/expo-github-action/fingerprint@main
|
||||
with:
|
||||
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
|
||||
args:
|
||||
|
||||
- name: 👀 Debug fingerprint
|
||||
id: fingerprint-debug
|
||||
@@ -90,13 +103,26 @@ jobs:
|
||||
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
|
||||
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
|
||||
|
||||
fingerprintDiff="${{ steps.fingerprint.outputs.fingerprint-diff }}"
|
||||
fingerprintDiff='$(echo "${{ steps.fingerprint.outputs.fingerprint-diff }}")'
|
||||
|
||||
if [[ $fingerprintDiff =~ "bareRncliAutolinking" || $fingerprintDiff =~ "expoAutolinkingAndroid" || $fingerprintDiff =~ "expoAutolinkingIos" ]]; then
|
||||
echo fingerprint-is-different="true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo fingerprint-is-different="false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Lint check
|
||||
run: yarn lint
|
||||
|
||||
- name: Prettier check
|
||||
run: yarn prettier --check .
|
||||
|
||||
- name: Check & compile i18n
|
||||
run: yarn intl:build
|
||||
|
||||
- name: Type check
|
||||
run: yarn typecheck
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
|
||||
@@ -113,10 +139,6 @@ jobs:
|
||||
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: 🔤 Compile Translations
|
||||
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
|
||||
run: yarn intl:build
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
|
||||
run: |
|
||||
@@ -136,11 +158,16 @@ jobs:
|
||||
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
||||
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
||||
|
||||
- name: Save successful deployment commit hash
|
||||
run: echo ${{ steps.fingerprint.outputs.current-git-commit }} > last-successful-commit-hash.txt
|
||||
|
||||
# GitHub actions are horrible so let's just copy paste this in
|
||||
buildIfNecessaryIOS:
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-14
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
||||
cancel-in-progress: false
|
||||
needs: [bundleDeploy]
|
||||
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||
# available here
|
||||
@@ -206,6 +233,9 @@ jobs:
|
||||
buildIfNecessaryAndroid:
|
||||
name: Build and Submit Android
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-android
|
||||
cancel-in-progress: false
|
||||
needs: [ bundleDeploy ]
|
||||
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||
# available here
|
||||
|
||||
@@ -13,13 +13,15 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
webpack-analyzer:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: write
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -94,11 +96,8 @@ jobs:
|
||||
|
||||
test-suite-fingerprint:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }}
|
||||
concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }}
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: write
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -199,7 +199,7 @@ describe('avoid-unwrapped-text', () => {
|
||||
|
||||
{
|
||||
code: `
|
||||
<View prop={
|
||||
<View propText={
|
||||
<Trans><Text>foo</Text></Trans>
|
||||
}>
|
||||
<Bar />
|
||||
@@ -281,6 +281,170 @@ function MyText({ foo }) {
|
||||
}
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
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: [
|
||||
@@ -455,6 +619,179 @@ function MyText({ foo }) {
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
{'foo'}
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
{foo && 'foo'}
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans>{'foo'}</Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
{foo && <Trans>{'foo'}</Trans>}
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
{10}
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans>{10}</Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans>{foo + 10}</Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
{\`foo\`}
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans>{\`foo\`}</Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans>{foo + \`foo\`}</Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
{_(msg\`foo\`)}
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
{foo + _(msg\`foo\`)}
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans>{_(msg\`foo\`)}</Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans>{foo + _(msg\`foo\`)}</Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans>foo</Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans><Trans>foo</Trans></Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans>{foo}</Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View>
|
||||
<Trans>{'foo'}</Trans>
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
|
||||
{
|
||||
code: `
|
||||
<View prop={
|
||||
<Trans><Text>foo</Text></Trans>
|
||||
}>
|
||||
<Bar />
|
||||
</View>
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ 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]
|
||||
|
||||
@@ -54,13 +55,13 @@ exports.create = function create(context) {
|
||||
return
|
||||
}
|
||||
if (tagName === 'Trans') {
|
||||
// Skip over it and check above.
|
||||
// Exit and rely on the traversal for <Trans> JSXElement (code below).
|
||||
// TODO: Maybe validate that it's present.
|
||||
parent = parent.parent
|
||||
continue
|
||||
return
|
||||
}
|
||||
let message = 'Wrap this string in <Text>.'
|
||||
if (tagName !== 'View') {
|
||||
const suggestedWrapper = suggestedTextWrappers[tagName]
|
||||
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
|
||||
if (tagName !== 'View' && !suggestedWrapper) {
|
||||
message +=
|
||||
' If <' +
|
||||
tagName +
|
||||
@@ -112,6 +113,189 @@ 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') {
|
||||
|
||||
+2
-2
@@ -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,7 @@
|
||||
"@mattermost/react-native-paste-input": "^0.6.4",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@react-native-async-storage/async-storage": "1.21.0",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-masked-view/masked-view": "0.3.0",
|
||||
"@react-native-menu/menu": "^0.8.0",
|
||||
"@react-native-picker/picker": "2.6.1",
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {LinearGradient} from 'expo-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'
|
||||
@@ -59,6 +58,10 @@ export type ButtonState = {
|
||||
|
||||
export type ButtonContext = VariantProps & ButtonState
|
||||
|
||||
type NonTextElements =
|
||||
| React.ReactElement
|
||||
| Iterable<React.ReactElement | null | undefined | boolean>
|
||||
|
||||
export type ButtonProps = Pick<
|
||||
PressableProps,
|
||||
'disabled' | 'onPress' | 'testID'
|
||||
@@ -68,11 +71,9 @@ export type ButtonProps = Pick<
|
||||
testID?: string
|
||||
label: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
children:
|
||||
| React.ReactNode
|
||||
| string
|
||||
| ((context: ButtonContext) => React.ReactNode | string)
|
||||
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
|
||||
}
|
||||
|
||||
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
||||
|
||||
const Context = React.createContext<VariantProps & ButtonState>({
|
||||
@@ -404,15 +405,7 @@ export function Button({
|
||||
</View>
|
||||
)}
|
||||
<Context.Provider value={context}>
|
||||
{/* @ts-ignore */}
|
||||
{typeof children === 'string' || children?.type === Trans ? (
|
||||
/* @ts-ignore */
|
||||
<ButtonText>{children}</ButtonText>
|
||||
) : typeof children === 'function' ? (
|
||||
children(context)
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
{typeof children === 'function' ? children(context) : children}
|
||||
</Context.Provider>
|
||||
</Pressable>
|
||||
)
|
||||
|
||||
@@ -39,7 +39,8 @@ export function useDialogControl(): DialogOuterProps['control'] {
|
||||
control.current.open()
|
||||
},
|
||||
close: cb => {
|
||||
control.current.close(cb)
|
||||
control.current.close()
|
||||
cb?.()
|
||||
},
|
||||
}),
|
||||
[id, control],
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import React, {useImperativeHandle} from 'react'
|
||||
import {View, TouchableWithoutFeedback} from 'react-native'
|
||||
import {FocusScope} from '@tamagui/focus-scope'
|
||||
import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
|
||||
import {TouchableWithoutFeedback, View} from 'react-native'
|
||||
import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {FocusScope} from '@tamagui/focus-scope'
|
||||
|
||||
import {useTheme, atoms as a, useBreakpoints, web, flatten} from '#/alf'
|
||||
import {logger} from '#/logger'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {Context} from '#/components/Dialog/context'
|
||||
import {
|
||||
DialogControlProps,
|
||||
DialogInnerProps,
|
||||
DialogOuterProps,
|
||||
} from '#/components/Dialog/types'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Portal} from '#/components/Portal'
|
||||
|
||||
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
|
||||
import {Context} from '#/components/Dialog/context'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
|
||||
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
|
||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||
export * from '#/components/Dialog/types'
|
||||
export {Input} from '#/components/forms/TextField'
|
||||
|
||||
@@ -37,14 +41,31 @@ export function Outer({
|
||||
setDialogIsOpen(control.id, true)
|
||||
}, [setIsOpen, setDialogIsOpen, control.id])
|
||||
|
||||
const close = React.useCallback(async () => {
|
||||
const onCloseInner = React.useCallback(async () => {
|
||||
setIsVisible(false)
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
setIsOpen(false)
|
||||
setIsVisible(true)
|
||||
setDialogIsOpen(control.id, false)
|
||||
onClose?.()
|
||||
}, [onClose, setIsOpen, setDialogIsOpen, control.id])
|
||||
}, [control.id, onClose, setDialogIsOpen])
|
||||
|
||||
const close = React.useCallback<DialogControlProps['close']>(
|
||||
cb => {
|
||||
try {
|
||||
if (cb && typeof cb === 'function') {
|
||||
cb()
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(`Dialog closeCallback failed`, {
|
||||
message: e.message,
|
||||
})
|
||||
} finally {
|
||||
onCloseInner()
|
||||
}
|
||||
},
|
||||
[onCloseInner],
|
||||
)
|
||||
|
||||
useImperativeHandle(
|
||||
control.ref,
|
||||
@@ -52,7 +73,7 @@ export function Outer({
|
||||
open,
|
||||
close,
|
||||
}),
|
||||
[open, close],
|
||||
[close, open],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -65,7 +86,7 @@ export function Outer({
|
||||
document.addEventListener('keydown', handler)
|
||||
|
||||
return () => document.removeEventListener('keydown', handler)
|
||||
}, [isOpen, close])
|
||||
}, [close, isOpen])
|
||||
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
@@ -82,7 +103,7 @@ export function Outer({
|
||||
<TouchableWithoutFeedback
|
||||
accessibilityHint={undefined}
|
||||
accessibilityLabel={_(msg`Close active dialog`)}
|
||||
onPress={close}>
|
||||
onPress={onCloseInner}>
|
||||
<View
|
||||
style={[
|
||||
web(a.fixed),
|
||||
|
||||
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Error} from '#/components/Error'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -87,7 +87,9 @@ function ListFooterMaybeError({
|
||||
a.py_sm,
|
||||
]}
|
||||
onPress={onRetry}>
|
||||
<Trans>Retry</Trans>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -91,15 +91,13 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
|
||||
}
|
||||
|
||||
export function Cancel({
|
||||
children,
|
||||
cta,
|
||||
}: React.PropsWithChildren<{
|
||||
}: {
|
||||
/**
|
||||
* Optional i18n string, used in lieu of `children` for simple buttons. If
|
||||
* undefined (and `children` is undefined), it will default to "Cancel".
|
||||
* Optional i18n string. If undefined, it will default to "Cancel".
|
||||
*/
|
||||
cta?: string
|
||||
}>) {
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {close} = Dialog.useDialogContext()
|
||||
@@ -114,33 +112,30 @@ export function Cancel({
|
||||
size={gtMobile ? 'small' : 'medium'}
|
||||
label={cta || _(msg`Cancel`)}
|
||||
onPress={onPress}>
|
||||
{children ? children : <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>}
|
||||
<ButtonText>{cta || _(msg`Cancel`)}</ButtonText>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function Action({
|
||||
children,
|
||||
onPress,
|
||||
color = 'primary',
|
||||
cta,
|
||||
testID,
|
||||
}: React.PropsWithChildren<{
|
||||
}: {
|
||||
onPress: () => void
|
||||
color?: ButtonColor
|
||||
/**
|
||||
* Optional i18n string, used in lieu of `children` for simple buttons. If
|
||||
* undefined (and `children` is undefined), it will default to "Confirm".
|
||||
* Optional i18n string. If undefined, it will default to "Confirm".
|
||||
*/
|
||||
cta?: string
|
||||
testID?: string
|
||||
}>) {
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {close} = Dialog.useDialogContext()
|
||||
const handleOnPress = React.useCallback(() => {
|
||||
close()
|
||||
onPress()
|
||||
close(onPress)
|
||||
}, [close, onPress])
|
||||
|
||||
return (
|
||||
@@ -151,7 +146,7 @@ export function Action({
|
||||
label={cta || _(msg`Confirm`)}
|
||||
onPress={handleOnPress}
|
||||
testID={testID}>
|
||||
{children ? children : <ButtonText>{cta || _(msg`Confirm`)}</ButtonText>}
|
||||
<ButtonText>{cta || _(msg`Confirm`)}</ButtonText>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import React from 'react'
|
||||
import {View, AccessibilityProps, TextStyle, ViewStyle} from 'react-native'
|
||||
import {AccessibilityProps, TextStyle, View, ViewStyle} from 'react-native'
|
||||
|
||||
import {atoms as a, useTheme, native} from '#/alf'
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
|
||||
export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
|
||||
AccessibilityProps &
|
||||
React.PropsWithChildren<{
|
||||
type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
|
||||
AccessibilityProps & {
|
||||
children: React.ReactElement
|
||||
testID?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & {
|
||||
multiple?: boolean
|
||||
@@ -47,49 +46,42 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
|
||||
const t = useTheme()
|
||||
const state = Toggle.useItemContext()
|
||||
|
||||
const {baseStyles, hoverStyles, activeStyles, textStyles} =
|
||||
React.useMemo(() => {
|
||||
const base: ViewStyle[] = []
|
||||
const hover: ViewStyle[] = []
|
||||
const active: ViewStyle[] = []
|
||||
const text: TextStyle[] = []
|
||||
const {baseStyles, hoverStyles, activeStyles} = React.useMemo(() => {
|
||||
const base: ViewStyle[] = []
|
||||
const hover: ViewStyle[] = []
|
||||
const active: ViewStyle[] = []
|
||||
|
||||
hover.push(
|
||||
t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25,
|
||||
)
|
||||
hover.push(
|
||||
t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25,
|
||||
)
|
||||
|
||||
if (state.selected) {
|
||||
active.push({
|
||||
backgroundColor: t.palette.contrast_800,
|
||||
})
|
||||
text.push(t.atoms.text_inverted)
|
||||
hover.push({
|
||||
backgroundColor: t.palette.contrast_800,
|
||||
})
|
||||
|
||||
if (state.disabled) {
|
||||
active.push({
|
||||
backgroundColor: t.palette.contrast_500,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (state.selected) {
|
||||
active.push({
|
||||
backgroundColor: t.palette.contrast_800,
|
||||
})
|
||||
hover.push({
|
||||
backgroundColor: t.palette.contrast_800,
|
||||
})
|
||||
|
||||
if (state.disabled) {
|
||||
base.push({
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
})
|
||||
text.push({
|
||||
opacity: 0.5,
|
||||
active.push({
|
||||
backgroundColor: t.palette.contrast_500,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
baseStyles: base,
|
||||
hoverStyles: hover,
|
||||
activeStyles: active,
|
||||
textStyles: text,
|
||||
}
|
||||
}, [t, state])
|
||||
if (state.disabled) {
|
||||
base.push({
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
baseStyles: base,
|
||||
hoverStyles: hover,
|
||||
activeStyles: active,
|
||||
}
|
||||
}, [t, state])
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -110,19 +102,37 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
|
||||
activeStyles,
|
||||
(state.hovered || state.pressed) && hoverStyles,
|
||||
]}>
|
||||
{typeof children === 'string' ? (
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
textStyles,
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonText({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
const state = Toggle.useItemContext()
|
||||
|
||||
const textStyles = React.useMemo(() => {
|
||||
const text: TextStyle[] = []
|
||||
if (state.selected) {
|
||||
text.push(t.atoms.text_inverted)
|
||||
}
|
||||
if (state.disabled) {
|
||||
text.push({
|
||||
opacity: 0.5,
|
||||
})
|
||||
}
|
||||
return text
|
||||
}, [t, state])
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
textStyles,
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,17 +84,17 @@ export function Buttons({
|
||||
onChange={onChange}>
|
||||
{ignoreLabel && (
|
||||
<ToggleButton.Button name="ignore" label={ignoreLabel}>
|
||||
{ignoreLabel}
|
||||
<ToggleButton.ButtonText>{ignoreLabel}</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
)}
|
||||
{warnLabel && (
|
||||
<ToggleButton.Button name="warn" label={warnLabel}>
|
||||
{warnLabel}
|
||||
<ToggleButton.ButtonText>{warnLabel}</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
)}
|
||||
{hideLabel && (
|
||||
<ToggleButton.Button name="hide" label={hideLabel}>
|
||||
{hideLabel}
|
||||
<ToggleButton.ButtonText>{hideLabel}</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
)}
|
||||
</ToggleButton.Group>
|
||||
|
||||
@@ -244,7 +244,7 @@ function AppealForm({
|
||||
size="medium"
|
||||
onPress={onPressBack}
|
||||
label={_(msg`Back`)}>
|
||||
{_(msg`Back`)}
|
||||
<ButtonText>{_(msg`Back`)}</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
testID="submitBtn"
|
||||
@@ -253,7 +253,7 @@ function AppealForm({
|
||||
size="medium"
|
||||
onPress={onSubmit}
|
||||
label={_(msg`Submit`)}>
|
||||
{_(msg`Submit`)}
|
||||
<ButtonText>{_(msg`Submit`)}</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -45,10 +45,12 @@ export type LogEvents = {
|
||||
'onboarding:moderation:nextPressed': {}
|
||||
'onboarding:finished:nextPressed': {}
|
||||
'feed:endReached': {
|
||||
feedUrl: string
|
||||
feedType: string
|
||||
itemCount: number
|
||||
}
|
||||
'feed:refresh': {
|
||||
feedUrl: string
|
||||
feedType: string
|
||||
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
|
||||
}
|
||||
@@ -65,6 +67,10 @@ export type LogEvents = {
|
||||
logContext: 'Composer'
|
||||
}
|
||||
'post:like': {
|
||||
doesLikerFollowPoster: boolean | undefined
|
||||
doesPosterFollowLiker: boolean | undefined
|
||||
likerClout: number | undefined
|
||||
postClout: number | undefined
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
}
|
||||
'post:repost': {
|
||||
@@ -77,6 +83,9 @@ export type LogEvents = {
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
}
|
||||
'profile:follow': {
|
||||
didBecomeMutual: boolean | undefined
|
||||
followeeClout: number | undefined
|
||||
followerClout: number | undefined
|
||||
logContext:
|
||||
| 'RecommendedFollowsItem'
|
||||
| 'PostThreadItem'
|
||||
|
||||
@@ -43,6 +43,14 @@ export function attachRouteToLogEvents(
|
||||
getCurrentRouteName = getRouteName
|
||||
}
|
||||
|
||||
export function toClout(n: number | null | undefined): number | undefined {
|
||||
if (n == null) {
|
||||
return undefined
|
||||
} else {
|
||||
return Math.max(0, Math.round(Math.log(n)))
|
||||
}
|
||||
}
|
||||
|
||||
export function logEvent<E extends keyof LogEvents>(
|
||||
eventName: E & string,
|
||||
rawMetadata: LogEvents[E] & FlatJSONRecord,
|
||||
@@ -78,6 +86,10 @@ function toStatsigUser(did: string | undefined) {
|
||||
return {
|
||||
userID,
|
||||
platform: Platform.OS,
|
||||
custom: {
|
||||
// Need to specify here too for gating.
|
||||
platform: Platform.OS,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {AccountList} from '#/components/AccountList'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {FormContainer} from './FormContainer'
|
||||
|
||||
@@ -75,7 +75,7 @@ export const ChooseAccountForm = ({
|
||||
color="secondary"
|
||||
size="medium"
|
||||
onPress={onPressBack}>
|
||||
{_(msg`Back`)}
|
||||
<ButtonText>{_(msg`Back`)}</ButtonText>
|
||||
</Button>
|
||||
<View style={[a.flex_1]} />
|
||||
</View>
|
||||
|
||||
@@ -21,9 +21,7 @@ export const LoginForm = ({
|
||||
error,
|
||||
serviceUrl,
|
||||
serviceDescription,
|
||||
setError,
|
||||
setServiceUrl,
|
||||
onPressRetryConnect,
|
||||
onPressBack,
|
||||
}: {
|
||||
error: string
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
useTheme,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import {leading, P, Text} from '#/components/Typography'
|
||||
@@ -73,7 +73,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
onPress={() => onboardDispatch({type: 'skip'})}
|
||||
// DEV ONLY
|
||||
label="Clear onboarding state">
|
||||
Clear
|
||||
<ButtonText>Clear</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {LabelPreference, InterpretedLabelValueDefinition} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
usePreferencesSetContentLabelMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ModerationOption({
|
||||
labelValueDefinition,
|
||||
@@ -83,13 +83,13 @@ export function ModerationOption({
|
||||
values={[visibility ?? 'hide']}
|
||||
onChange={onChange}>
|
||||
<ToggleButton.Button name="ignore" label={labels.show}>
|
||||
{labels.show}
|
||||
<ToggleButton.ButtonText>{labels.show}</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="warn" label={labels.warn}>
|
||||
{labels.warn}
|
||||
<ToggleButton.ButtonText>{labels.warn}</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="hide" label={labels.hide}>
|
||||
{labels.hide}
|
||||
<ToggleButton.ButtonText>{labels.hide}</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,9 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
@@ -78,6 +80,9 @@ let ProfileHeaderStandard = ({
|
||||
})
|
||||
}, [track, openModal, profile])
|
||||
|
||||
const autoExpandSuggestionsOnProfileFollow = useGate(
|
||||
'autoexpand_suggestions_on_profile_follow',
|
||||
)
|
||||
const onPressFollow = () => {
|
||||
requireAuth(async () => {
|
||||
try {
|
||||
@@ -91,6 +96,9 @@ let ProfileHeaderStandard = ({
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
if (isWeb && autoExpandSuggestionsOnProfileFollow) {
|
||||
setShowSuggestedFollows(true)
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to follow', {message: String(e)})
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {useCallback} from 'react'
|
||||
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {track} from '#/lib/analytics/analytics'
|
||||
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
|
||||
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
|
||||
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
|
||||
import {updatePostShadow} from '#/state/cache/post-shadow'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {findProfileQueryData} from './profile'
|
||||
|
||||
const RQKEY_ROOT = 'post'
|
||||
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
|
||||
@@ -68,7 +69,7 @@ export function usePostLikeMutationQueue(
|
||||
const postUri = post.uri
|
||||
const postCid = post.cid
|
||||
const initialLikeUri = post.viewer?.like
|
||||
const likeMutation = usePostLikeMutation(logContext)
|
||||
const likeMutation = usePostLikeMutation(logContext, post)
|
||||
const unlikeMutation = usePostUnlikeMutation(logContext)
|
||||
|
||||
const queueToggle = useToggleMutationQueue({
|
||||
@@ -117,15 +118,40 @@ export function usePostLikeMutationQueue(
|
||||
return [queueLike, queueUnlike]
|
||||
}
|
||||
|
||||
function usePostLikeMutation(logContext: LogEvents['post:like']['logContext']) {
|
||||
function usePostLikeMutation(
|
||||
logContext: LogEvents['post:like']['logContext'],
|
||||
post: Shadow<AppBskyFeedDefs.PostView>,
|
||||
) {
|
||||
const {currentAccount} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
const postAuthor = post.author
|
||||
return useMutation<
|
||||
{uri: string}, // responds with the uri of the like
|
||||
Error,
|
||||
{uri: string; cid: string} // the post's uri and cid
|
||||
>({
|
||||
mutationFn: post => {
|
||||
logEvent('post:like', {logContext})
|
||||
return getAgent().like(post.uri, post.cid)
|
||||
mutationFn: ({uri, cid}) => {
|
||||
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
|
||||
if (currentAccount) {
|
||||
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
|
||||
}
|
||||
logEvent('post:like', {
|
||||
logContext,
|
||||
doesPosterFollowLiker: postAuthor.viewer
|
||||
? Boolean(postAuthor.viewer.followedBy)
|
||||
: undefined,
|
||||
doesLikerFollowPoster: postAuthor.viewer
|
||||
? Boolean(postAuthor.viewer.following)
|
||||
: undefined,
|
||||
likerClout: toClout(ownProfile?.followersCount),
|
||||
postClout:
|
||||
post.likeCount != null &&
|
||||
post.repostCount != null &&
|
||||
post.replyCount != null
|
||||
? toClout(post.likeCount + post.repostCount + post.replyCount)
|
||||
: undefined,
|
||||
})
|
||||
return getAgent().like(uri, cid)
|
||||
},
|
||||
onSuccess() {
|
||||
track('Post:Like')
|
||||
|
||||
@@ -20,7 +20,7 @@ import {track} from '#/lib/analytics/analytics'
|
||||
import {uploadBlob} from '#/lib/api'
|
||||
import {until} from '#/lib/async/until'
|
||||
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
|
||||
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
|
||||
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
|
||||
@@ -202,7 +202,7 @@ export function useProfileFollowMutationQueue(
|
||||
const queryClient = useQueryClient()
|
||||
const did = profile.did
|
||||
const initialFollowingUri = profile.viewer?.following
|
||||
const followMutation = useProfileFollowMutation(logContext)
|
||||
const followMutation = useProfileFollowMutation(logContext, profile)
|
||||
const unfollowMutation = useProfileUnfollowMutation(logContext)
|
||||
|
||||
const queueToggle = useToggleMutationQueue({
|
||||
@@ -252,10 +252,24 @@ export function useProfileFollowMutationQueue(
|
||||
|
||||
function useProfileFollowMutation(
|
||||
logContext: LogEvents['profile:follow']['logContext'],
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
|
||||
) {
|
||||
const {currentAccount} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
||||
mutationFn: async ({did}) => {
|
||||
logEvent('profile:follow', {logContext})
|
||||
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
|
||||
if (currentAccount) {
|
||||
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
|
||||
}
|
||||
logEvent('profile:follow', {
|
||||
logContext,
|
||||
didBecomeMutual: profile.viewer
|
||||
? Boolean(profile.viewer.followedBy)
|
||||
: undefined,
|
||||
followeeClout: toClout(profile.followersCount),
|
||||
followerClout: toClout(ownProfile?.followersCount),
|
||||
})
|
||||
return await getAgent().follow(did)
|
||||
},
|
||||
onSuccess(data, variables) {
|
||||
@@ -530,3 +544,12 @@ export function* findAllProfilesInQueryData(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function findProfileQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
): AppBskyActorDefs.ProfileViewDetailed | undefined {
|
||||
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewDetailed>(
|
||||
RQKEY(did),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react'
|
||||
import * as persisted from '#/state/persisted'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
|
||||
type StateContext = string
|
||||
type SetContext = (v: string) => void
|
||||
@@ -8,7 +10,7 @@ type SetContext = (v: string) => void
|
||||
const stateContext = React.createContext<StateContext>('home')
|
||||
const setContext = React.createContext<SetContext>((_: string) => {})
|
||||
|
||||
function getInitialFeed() {
|
||||
function getInitialFeed(startSessionWithFollowing: boolean) {
|
||||
if (isWeb) {
|
||||
if (window.location.pathname === '/') {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
@@ -24,16 +26,21 @@ function getInitialFeed() {
|
||||
return feedFromSession
|
||||
}
|
||||
}
|
||||
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
|
||||
if (feedFromPersisted) {
|
||||
// Fall back to the last chosen one across all tabs.
|
||||
return feedFromPersisted
|
||||
if (!startSessionWithFollowing) {
|
||||
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
|
||||
if (feedFromPersisted) {
|
||||
// Fall back to the last chosen one across all tabs.
|
||||
return feedFromPersisted
|
||||
}
|
||||
}
|
||||
return 'home'
|
||||
}
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const [state, setState] = React.useState(getInitialFeed)
|
||||
const startSessionWithFollowing = useGate('start_session_with_following')
|
||||
const [state, setState] = React.useState(() =>
|
||||
getInitialFeed(startSessionWithFollowing),
|
||||
)
|
||||
|
||||
const saveState = React.useCallback((feed: string) => {
|
||||
setState(feed)
|
||||
|
||||
@@ -87,13 +87,17 @@ export function ServerInputDialog({
|
||||
values={fixedOption}
|
||||
onChange={setFixedOption}>
|
||||
<ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}>
|
||||
{_(msg`Bluesky`)}
|
||||
<ToggleButton.ButtonText>
|
||||
{_(msg`Bluesky`)}
|
||||
</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button
|
||||
testID="customSelectBtn"
|
||||
name="custom"
|
||||
label={_(msg`Custom`)}>
|
||||
{_(msg`Custom`)}
|
||||
<ToggleButton.ButtonText>
|
||||
{_(msg`Custom`)}
|
||||
</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
|
||||
@@ -163,7 +167,7 @@ export function ServerInputDialog({
|
||||
size="small"
|
||||
onPress={() => control.close()}
|
||||
label={_(msg`Done`)}>
|
||||
{_(msg`Done`)}
|
||||
<ButtonText>{_(msg`Done`)}</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -508,11 +508,7 @@ export const ComposePost = observer(function ComposePost({
|
||||
title={_(msg`Discard draft?`)}
|
||||
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
||||
onConfirm={() => {
|
||||
if (isWeb) {
|
||||
onClose()
|
||||
} else {
|
||||
discardPromptControl.close(onClose)
|
||||
}
|
||||
discardPromptControl.close(onClose)
|
||||
}}
|
||||
confirmButtonCta={_(msg`Discard`)}
|
||||
confirmButtonColor="negative"
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import React from 'react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||
import {MainScrollProvider} from '../util/MainScrollProvider'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
|
||||
import {ComposeIcon2} from 'lib/icons'
|
||||
import {s} from 'lib/styles'
|
||||
import {View, useWindowDimensions} from 'react-native'
|
||||
import {ListMethods} from '../util/List'
|
||||
import {Feed} from '../posts/Feed'
|
||||
import {FAB} from '../util/fab/FAB'
|
||||
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
|
||||
import {useWindowDimensions, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
|
||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
|
||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {ComposeIcon2} from 'lib/icons'
|
||||
import {s} from 'lib/styles'
|
||||
import {Feed} from '../posts/Feed'
|
||||
import {FAB} from '../util/fab/FAB'
|
||||
import {ListMethods} from '../util/List'
|
||||
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
|
||||
import {MainScrollProvider} from '../util/MainScrollProvider'
|
||||
|
||||
const POLL_FREQ = 60e3 // 60sec
|
||||
|
||||
@@ -71,6 +72,7 @@ export function FeedPage({
|
||||
setHasNew(false)
|
||||
logEvent('feed:refresh', {
|
||||
feedType: feed.split('|')[0],
|
||||
feedUrl: feed,
|
||||
reason: 'soft-reset',
|
||||
})
|
||||
}
|
||||
@@ -96,10 +98,22 @@ export function FeedPage({
|
||||
setHasNew(false)
|
||||
logEvent('feed:refresh', {
|
||||
feedType: feed.split('|')[0],
|
||||
feedUrl: feed,
|
||||
reason: 'load-latest',
|
||||
})
|
||||
}, [scrollToTop, feed, queryClient, setHasNew])
|
||||
|
||||
let feedPollInterval
|
||||
if (
|
||||
useGate('disable_poll_on_discover') &&
|
||||
feed === // Discover
|
||||
'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
|
||||
) {
|
||||
feedPollInterval = undefined
|
||||
} else {
|
||||
feedPollInterval = POLL_FREQ
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID={testID} style={s.h100pct}>
|
||||
<MainScrollProvider>
|
||||
@@ -108,7 +122,7 @@ export function FeedPage({
|
||||
enabled={isPageFocused}
|
||||
feed={feed}
|
||||
feedParams={feedParams}
|
||||
pollInterval={POLL_FREQ}
|
||||
pollInterval={feedPollInterval}
|
||||
disablePoll={hasNew}
|
||||
scrollElRef={scrollElRef}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {s} from 'lib/styles'
|
||||
import {track} from 'lib/analytics/analytics'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {s} from 'lib/styles'
|
||||
import {Shadow, useProfileShadow} from 'state/cache/profile-shadow'
|
||||
import {track} from 'lib/analytics/analytics'
|
||||
import {
|
||||
useProfileFollowMutationQueue,
|
||||
useProfileQuery,
|
||||
} from 'state/queries/profile'
|
||||
import {useRequireAuth} from 'state/session'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
|
||||
export function PostThreadFollowBtn({did}: {did: string}) {
|
||||
const {data: profile, isLoading} = useProfileQuery({did})
|
||||
@@ -47,8 +48,10 @@ function PostThreadFollowBtnLoaded({
|
||||
'PostThreadItem',
|
||||
)
|
||||
const requireAuth = useRequireAuth()
|
||||
const showFollowBackLabel = useGate('show_follow_back_label')
|
||||
|
||||
const isFollowing = !!profile.viewer?.following
|
||||
const isFollowedBy = !!profile.viewer?.followedBy
|
||||
const [wasFollowing, setWasFollowing] = React.useState<boolean>(isFollowing)
|
||||
|
||||
// This prevents the button from disappearing as soon as we follow.
|
||||
@@ -136,7 +139,15 @@ function PostThreadFollowBtnLoaded({
|
||||
type="button"
|
||||
style={[!isFollowing ? palInverted.text : pal.text, s.bold]}
|
||||
numberOfLines={1}>
|
||||
{!isFollowing ? <Trans>Follow</Trans> : <Trans>Following</Trans>}
|
||||
{!isFollowing ? (
|
||||
showFollowBackLabel && isFollowedBy ? (
|
||||
<Trans>Follow Back</Trans>
|
||||
) : (
|
||||
<Trans>Follow</Trans>
|
||||
)
|
||||
) : (
|
||||
<Trans>Following</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
+27
-23
@@ -8,32 +8,33 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {List, ListRef} from '../util/List'
|
||||
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {FeedErrorMessage} from './FeedErrorMessage'
|
||||
import {FeedSlice} from './FeedSlice'
|
||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {logger} from '#/logger'
|
||||
import {
|
||||
RQKEY,
|
||||
FeedDescriptor,
|
||||
FeedParams,
|
||||
usePostFeedQuery,
|
||||
pollLatest,
|
||||
} from '#/state/queries/post-feed'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {listenPostCreated} from '#/state/events'
|
||||
import {useSession} from '#/state/session'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
|
||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {listenPostCreated} from '#/state/events'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {
|
||||
FeedDescriptor,
|
||||
FeedParams,
|
||||
pollLatest,
|
||||
RQKEY,
|
||||
usePostFeedQuery,
|
||||
} from '#/state/queries/post-feed'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {List, ListRef} from '../util/List'
|
||||
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
||||
import {FeedErrorMessage} from './FeedErrorMessage'
|
||||
import {FeedSlice} from './FeedSlice'
|
||||
|
||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||
@@ -217,6 +218,7 @@ let Feed = ({
|
||||
track('Feed:onRefresh')
|
||||
logEvent('feed:refresh', {
|
||||
feedType: feedType,
|
||||
feedUrl: feed,
|
||||
reason: 'pull-to-refresh',
|
||||
})
|
||||
setIsPTRing(true)
|
||||
@@ -227,13 +229,14 @@ let Feed = ({
|
||||
logger.error('Failed to refresh posts feed', {message: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, track, setIsPTRing, onHasNew, feedType])
|
||||
}, [refetch, track, setIsPTRing, onHasNew, feed, feedType])
|
||||
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
|
||||
logEvent('feed:endReached', {
|
||||
feedType: feedType,
|
||||
feedUrl: feed,
|
||||
itemCount: feedItems.length,
|
||||
})
|
||||
track('Feed:onEndReached')
|
||||
@@ -248,6 +251,7 @@ let Feed = ({
|
||||
isError,
|
||||
fetchNextPage,
|
||||
track,
|
||||
feed,
|
||||
feedType,
|
||||
feedItems.length,
|
||||
])
|
||||
|
||||
@@ -274,13 +274,13 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
|
||||
values={scenario}
|
||||
onChange={setScenario}>
|
||||
<ToggleButton.Button name="label" label="Label">
|
||||
Label
|
||||
<ToggleButton.ButtonText>Label</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="block" label="Block">
|
||||
Block
|
||||
<ToggleButton.ButtonText>Block</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="mute" label="Mute">
|
||||
Mute
|
||||
<ToggleButton.ButtonText>Mute</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
|
||||
@@ -474,16 +474,16 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
|
||||
|
||||
<ToggleButton.Group label="Results" values={view} onChange={setView}>
|
||||
<ToggleButton.Button name="post" label="Post">
|
||||
Post
|
||||
<ToggleButton.ButtonText>Post</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="notifications" label="Notifications">
|
||||
Notifications
|
||||
<ToggleButton.ButtonText>Notifications</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="account" label="Account">
|
||||
Account
|
||||
<ToggleButton.ButtonText>Account</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="data" label="Data">
|
||||
Data
|
||||
<ToggleButton.ButtonText>Data</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
|
||||
|
||||
+32
-14
@@ -1,23 +1,25 @@
|
||||
import React from 'react'
|
||||
import {View, ActivityIndicator, StyleSheet} from 'react-native'
|
||||
import {ActivityIndicator, AppState, StyleSheet, View} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
|
||||
|
||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {FeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed'
|
||||
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
|
||||
import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||
import {FeedPage} from 'view/com/feeds/FeedPage'
|
||||
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
|
||||
import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
|
||||
import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
|
||||
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
|
||||
import {HomeHeader} from '../com/home/HomeHeader'
|
||||
import {Pager, RenderTabBarFnProps, PagerRef} from 'view/com/pager/Pager'
|
||||
import {FeedPage} from 'view/com/feeds/FeedPage'
|
||||
import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA'
|
||||
import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {usePinnedFeedsInfos, FeedSourceInfo} from '#/state/queries/feed'
|
||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
|
||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
import {HomeHeader} from '../com/home/HomeHeader'
|
||||
|
||||
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
|
||||
export function HomeScreen(props: Props) {
|
||||
@@ -94,6 +96,22 @@ function HomeScreenReady({
|
||||
}, [setDrawerSwipeDisabled, selectedIndex, setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const disableMinShellOnForegrounding = useGate(
|
||||
'disable_min_shell_on_foregrounding',
|
||||
)
|
||||
React.useEffect(() => {
|
||||
if (disableMinShellOnForegrounding) {
|
||||
const listener = AppState.addEventListener('change', nextAppState => {
|
||||
if (nextAppState === 'active') {
|
||||
setMinimalShellMode(false)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
listener.remove()
|
||||
}
|
||||
}
|
||||
}, [setMinimalShellMode, disableMinShellOnForegrounding])
|
||||
|
||||
const onPageSelected = React.useCallback(
|
||||
(index: number) => {
|
||||
setMinimalShellMode(false)
|
||||
|
||||
@@ -92,7 +92,9 @@ export function ExportCarDialog({
|
||||
size={gtMobile ? 'small' : 'large'}
|
||||
onPress={() => control.close()}
|
||||
label={_(msg`Done`)}>
|
||||
{_(msg`Done`)}
|
||||
<ButtonText>
|
||||
<Trans>Done</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ import {View} from 'react-native'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
ButtonColor,
|
||||
ButtonIcon,
|
||||
ButtonText,
|
||||
ButtonVariant,
|
||||
} from '#/components/Button'
|
||||
import {H1} from '#/components/Typography'
|
||||
import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||
import {H1} from '#/components/Typography'
|
||||
|
||||
export function Buttons() {
|
||||
return (
|
||||
@@ -29,7 +29,7 @@ export function Buttons() {
|
||||
color={color as ButtonColor}
|
||||
size="large"
|
||||
label="Click here">
|
||||
Button
|
||||
<ButtonText>Button</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
@@ -37,7 +37,7 @@ export function Buttons() {
|
||||
color={color as ButtonColor}
|
||||
size="large"
|
||||
label="Click here">
|
||||
Button
|
||||
<ButtonText>Button</ButtonText>
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
@@ -54,7 +54,7 @@ export function Buttons() {
|
||||
color={name as ButtonColor}
|
||||
size="large"
|
||||
label="Click here">
|
||||
Button
|
||||
<ButtonText>Button</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
@@ -62,7 +62,7 @@ export function Buttons() {
|
||||
color={name as ButtonColor}
|
||||
size="large"
|
||||
label="Click here">
|
||||
Button
|
||||
<ButtonText>Button</ButtonText>
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
),
|
||||
@@ -77,7 +77,7 @@ export function Buttons() {
|
||||
color={name as ButtonColor}
|
||||
size="large"
|
||||
label="Click here">
|
||||
Button
|
||||
<ButtonText>Button</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
@@ -85,7 +85,7 @@ export function Buttons() {
|
||||
color={name as ButtonColor}
|
||||
size="large"
|
||||
label="Click here">
|
||||
Button
|
||||
<ButtonText>Button</ButtonText>
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
),
|
||||
|
||||
@@ -3,7 +3,7 @@ import {View} from 'react-native'
|
||||
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {H3, P} from '#/components/Typography'
|
||||
@@ -26,7 +26,7 @@ export function Dialogs() {
|
||||
basic.open()
|
||||
}}
|
||||
label="Open basic dialog">
|
||||
Open all dialogs
|
||||
<ButtonText>Open all dialogs</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -37,7 +37,7 @@ export function Dialogs() {
|
||||
scrollable.open()
|
||||
}}
|
||||
label="Open basic dialog">
|
||||
Open scrollable dialog
|
||||
<ButtonText>Open scrollable dialog</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -48,7 +48,7 @@ export function Dialogs() {
|
||||
basic.open()
|
||||
}}
|
||||
label="Open basic dialog">
|
||||
Open basic dialog
|
||||
<ButtonText>Open basic dialog</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -57,7 +57,7 @@ export function Dialogs() {
|
||||
size="small"
|
||||
onPress={() => prompt.open()}
|
||||
label="Open prompt">
|
||||
Open prompt
|
||||
<ButtonText>Open prompt</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Prompt.Outer control={prompt}>
|
||||
@@ -67,8 +67,8 @@ export function Dialogs() {
|
||||
description, as well as two actions.
|
||||
</Prompt.DescriptionText>
|
||||
<Prompt.Actions>
|
||||
<Prompt.Cancel>Cancel</Prompt.Cancel>
|
||||
<Prompt.Action onPress={() => {}}>Confirm</Prompt.Action>
|
||||
<Prompt.Cancel />
|
||||
<Prompt.Action cta="Confirm" onPress={() => {}} />
|
||||
</Prompt.Actions>
|
||||
</Prompt.Outer>
|
||||
|
||||
@@ -102,7 +102,7 @@ export function Dialogs() {
|
||||
size="small"
|
||||
onPress={closeAllDialogs}
|
||||
label="Close all dialogs">
|
||||
Close all dialogs
|
||||
<ButtonText>Close all dialogs</ButtonText>
|
||||
</Button>
|
||||
<View style={{height: 1000}} />
|
||||
<View style={[a.flex_row, a.justify_end]}>
|
||||
@@ -116,7 +116,7 @@ export function Dialogs() {
|
||||
})
|
||||
}
|
||||
label="Open basic dialog">
|
||||
Close dialog
|
||||
<ButtonText>Close dialog</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {DateField, LabelText} from '#/components/forms/DateField'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
@@ -191,7 +191,7 @@ export function Forms() {
|
||||
setToggleGroupBValues(['a', 'b'])
|
||||
setToggleGroupCValues(['a'])
|
||||
}}>
|
||||
Reset all toggles
|
||||
<ButtonText>Reset all toggles</ButtonText>
|
||||
</Button>
|
||||
|
||||
<View style={[a.gap_md, a.align_start, a.w_full]}>
|
||||
@@ -202,13 +202,13 @@ export function Forms() {
|
||||
values={toggleGroupDValues}
|
||||
onChange={setToggleGroupDValues}>
|
||||
<ToggleButton.Button name="hide" label="Hide">
|
||||
Hide
|
||||
<ToggleButton.ButtonText>Hide</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="warn" label="Warn">
|
||||
Warn
|
||||
<ToggleButton.ButtonText>Warn</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="show" label="Show">
|
||||
Show
|
||||
<ToggleButton.ButtonText>Show</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
|
||||
@@ -218,13 +218,13 @@ export function Forms() {
|
||||
values={toggleGroupDValues}
|
||||
onChange={setToggleGroupDValues}>
|
||||
<ToggleButton.Button name="hide" label="Hide">
|
||||
Hide
|
||||
<ToggleButton.ButtonText>Hide</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="warn" label="Warn">
|
||||
Warn
|
||||
<ToggleButton.ButtonText>Warn</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="show" label="Show">
|
||||
Show
|
||||
<ToggleButton.ButtonText>Show</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
</View>
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||
|
||||
import {atoms as a, useTheme, ThemeProvider} from '#/alf'
|
||||
import {useSetThemePrefs} from '#/state/shell'
|
||||
import {Button} from '#/components/Button'
|
||||
|
||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||
import {atoms as a, ThemeProvider, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Breakpoints} from './Breakpoints'
|
||||
import {Buttons} from './Buttons'
|
||||
import {Dialogs} from './Dialogs'
|
||||
import {Forms} from './Forms'
|
||||
import {Icons} from './Icons'
|
||||
import {Links} from './Links'
|
||||
import {Menus} from './Menus'
|
||||
import {Shadows} from './Shadows'
|
||||
import {Spacing} from './Spacing'
|
||||
import {Theming} from './Theming'
|
||||
import {Typography} from './Typography'
|
||||
import {Spacing} from './Spacing'
|
||||
import {Buttons} from './Buttons'
|
||||
import {Links} from './Links'
|
||||
import {Forms} from './Forms'
|
||||
import {Dialogs} from './Dialogs'
|
||||
import {Breakpoints} from './Breakpoints'
|
||||
import {Shadows} from './Shadows'
|
||||
import {Icons} from './Icons'
|
||||
import {Menus} from './Menus'
|
||||
|
||||
export function Storybook() {
|
||||
const t = useTheme()
|
||||
@@ -33,7 +32,7 @@ export function Storybook() {
|
||||
size="small"
|
||||
label='Set theme to "system"'
|
||||
onPress={() => setColorMode('system')}>
|
||||
System
|
||||
<ButtonText>System</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
@@ -41,7 +40,7 @@ export function Storybook() {
|
||||
size="small"
|
||||
label='Set theme to "light"'
|
||||
onPress={() => setColorMode('light')}>
|
||||
Light
|
||||
<ButtonText>Light</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
@@ -52,7 +51,7 @@ export function Storybook() {
|
||||
setColorMode('dark')
|
||||
setDarkTheme('dim')
|
||||
}}>
|
||||
Dim
|
||||
<ButtonText>Dim</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
@@ -63,7 +62,7 @@ export function Storybook() {
|
||||
setColorMode('dark')
|
||||
setDarkTheme('dark')
|
||||
}}>
|
||||
Dark
|
||||
<ButtonText>Dark</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -4836,10 +4836,10 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@react-native-async-storage/async-storage@1.21.0":
|
||||
version "1.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.21.0.tgz#d7e370028e228ab84637016ceeb495878b7a44c8"
|
||||
integrity sha512-JL0w36KuFHFCvnbOXRekqVAUplmOyT/OuCQkogo6X98MtpSaJOKEAeZnYO8JB0U/RIEixZaGI5px73YbRm/oag==
|
||||
"@react-native-async-storage/async-storage@1.23.1":
|
||||
version "1.23.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz#cad3cd4fab7dacfe9838dce6ecb352f79150c883"
|
||||
integrity sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==
|
||||
dependencies:
|
||||
merge-options "^3.0.4"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user