Merge branch 'main' into hailey/oauth

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