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': [
'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': [
+39 -9
View File
@@ -22,6 +22,9 @@ jobs:
bundleDeploy:
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
cancel-in-progress: true
outputs:
fingerprint-is-different: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different }}
@@ -44,7 +47,18 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 100
fetch-depth: 0
- name: ⬇️ Get last successful deployment commit from the cache
id: get-base-commit
uses: actions/cache@v4
with:
path: last-successful-commit-hash.txt
key: last-successful-deployment-commit-${{ github.ref_name }}
- name: Add the last successful deployment commit to the output
id: last-successful-commit
run: echo base-commit=$(cat last-successful-commit-hash.txt) >> "$GITHUB_OUTPUT"
- name: ⬇️ Fetch commits from base branch
if: ${{ github.ref != 'refs/heads/main' }}
@@ -57,12 +71,12 @@ jobs:
if [ -z "${{ inputs.channel == 'production' }}" ]; then
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
else
echo base-commit=$(git log -n 1 --skip 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
echo base-commit=${{ steps.last-successful-commit.base-commit }} >> "$GITHUB_OUTPUT"
fi
- name: ✓ Make sure we found a base commit
run: |
if [ -z "${{ steps.base-commit.outputs.base-commit }}" ]; then
if [ -z "${{ steps.base-commit.outputs.base-commit }}" && ${{ inputs.channel == 'production' }} ]; then
echo "Could not find a base commit for this release. Exiting."
exit 1
fi
@@ -82,7 +96,6 @@ jobs:
uses: expo/expo-github-action/fingerprint@main
with:
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
args:
- name: 👀 Debug fingerprint
id: fingerprint-debug
@@ -90,13 +103,26 @@ jobs:
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
fingerprintDiff="${{ steps.fingerprint.outputs.fingerprint-diff }}"
fingerprintDiff='$(echo "${{ steps.fingerprint.outputs.fingerprint-diff }}")'
if [[ $fingerprintDiff =~ "bareRncliAutolinking" || $fingerprintDiff =~ "expoAutolinkingAndroid" || $fingerprintDiff =~ "expoAutolinkingIos" ]]; then
echo fingerprint-is-different="true" >> "$GITHUB_OUTPUT"
else
echo fingerprint-is-different="false" >> "$GITHUB_OUTPUT"
fi
- name: Lint check
run: yarn lint
- name: Prettier check
run: yarn prettier --check .
- name: Check & compile i18n
run: yarn intl:build
- name: Type check
run: yarn typecheck
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
@@ -113,10 +139,6 @@ jobs:
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
uses: dcarbone/install-jq-action@v2
- name: 🔤 Compile Translations
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn intl:build
- name: ✏️ Write environment variables
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: |
@@ -136,11 +158,16 @@ jobs:
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
- name: Save successful deployment commit hash
run: echo ${{ steps.fingerprint.outputs.current-git-commit }} > last-successful-commit-hash.txt
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
name: Build and Submit iOS
runs-on: macos-14
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
cancel-in-progress: false
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
@@ -206,6 +233,9 @@ jobs:
buildIfNecessaryAndroid:
name: Build and Submit Android
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-android
cancel-in-progress: false
needs: [ bundleDeploy ]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
+7 -8
View File
@@ -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
+338 -1
View File
@@ -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,
},
],
}
+189 -5
View File
@@ -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
View File
@@ -35,7 +35,7 @@
"e2e:run": "NODE_ENV=test detox test --configuration ios.sim.debug --take-screenshots all",
"perf:test": "NODE_ENV=test maestro test",
"perf:test:run": "NODE_ENV=test maestro test __e2e__/maestro/scroll.yaml",
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand \"yarn perf:test\" --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure",
"intl:build": "yarn intl:extract && yarn intl:compile",
@@ -64,7 +64,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",
+7 -14
View File
@@ -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>
)
+2 -1
View File
@@ -39,7 +39,8 @@ export function useDialogControl(): DialogOuterProps['control'] {
control.current.open()
},
close: cb => {
control.current.close(cb)
control.current.close()
cb?.()
},
}),
[id, control],
+37 -16
View File
@@ -1,20 +1,24 @@
import React, {useImperativeHandle} from 'react'
import {View, TouchableWithoutFeedback} from 'react-native'
import {FocusScope} from '@tamagui/focus-scope'
import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
import {TouchableWithoutFeedback, View} from 'react-native'
import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FocusScope} from '@tamagui/focus-scope'
import {useTheme, atoms as a, useBreakpoints, web, flatten} from '#/alf'
import {logger} from '#/logger'
import {useDialogStateControlContext} from '#/state/dialogs'
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {Context} from '#/components/Dialog/context'
import {
DialogControlProps,
DialogInnerProps,
DialogOuterProps,
} from '#/components/Dialog/types'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Portal} from '#/components/Portal'
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
import {Context} from '#/components/Dialog/context'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {useDialogStateControlContext} from '#/state/dialogs'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
export {Input} from '#/components/forms/TextField'
@@ -37,14 +41,31 @@ export function Outer({
setDialogIsOpen(control.id, true)
}, [setIsOpen, setDialogIsOpen, control.id])
const close = React.useCallback(async () => {
const onCloseInner = React.useCallback(async () => {
setIsVisible(false)
await new Promise(resolve => setTimeout(resolve, 150))
setIsOpen(false)
setIsVisible(true)
setDialogIsOpen(control.id, false)
onClose?.()
}, [onClose, setIsOpen, setDialogIsOpen, control.id])
}, [control.id, onClose, setDialogIsOpen])
const close = React.useCallback<DialogControlProps['close']>(
cb => {
try {
if (cb && typeof cb === 'function') {
cb()
}
} catch (e: any) {
logger.error(`Dialog closeCallback failed`, {
message: e.message,
})
} finally {
onCloseInner()
}
},
[onCloseInner],
)
useImperativeHandle(
control.ref,
@@ -52,7 +73,7 @@ export function Outer({
open,
close,
}),
[open, close],
[close, open],
)
React.useEffect(() => {
@@ -65,7 +86,7 @@ export function Outer({
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [isOpen, close])
}, [close, isOpen])
const context = React.useMemo(
() => ({
@@ -82,7 +103,7 @@ export function Outer({
<TouchableWithoutFeedback
accessibilityHint={undefined}
accessibilityLabel={_(msg`Close active dialog`)}
onPress={close}>
onPress={onCloseInner}>
<View
style={[
web(a.fixed),
+4 -2
View File
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
import {cleanError} from 'lib/strings/errors'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Button, ButtonText} from '#/components/Button'
import {Error} from '#/components/Error'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
@@ -87,7 +87,9 @@ function ListFooterMaybeError({
a.py_sm,
]}
onPress={onRetry}>
<Trans>Retry</Trans>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
</Button>
</View>
</View>
+9 -14
View File
@@ -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>
)
}
+67 -57
View File
@@ -1,16 +1,15 @@
import React from 'react'
import {View, AccessibilityProps, TextStyle, ViewStyle} from 'react-native'
import {AccessibilityProps, TextStyle, View, ViewStyle} from 'react-native'
import {atoms as a, useTheme, native} from '#/alf'
import {atoms as a, native, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
AccessibilityProps &
React.PropsWithChildren<{
type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
AccessibilityProps & {
children: React.ReactElement
testID?: string
}>
}
export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & {
multiple?: boolean
@@ -47,49 +46,42 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const state = Toggle.useItemContext()
const {baseStyles, hoverStyles, activeStyles, textStyles} =
React.useMemo(() => {
const base: ViewStyle[] = []
const hover: ViewStyle[] = []
const active: ViewStyle[] = []
const text: TextStyle[] = []
const {baseStyles, hoverStyles, activeStyles} = React.useMemo(() => {
const base: ViewStyle[] = []
const hover: ViewStyle[] = []
const active: ViewStyle[] = []
hover.push(
t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25,
)
hover.push(
t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25,
)
if (state.selected) {
active.push({
backgroundColor: t.palette.contrast_800,
})
text.push(t.atoms.text_inverted)
hover.push({
backgroundColor: t.palette.contrast_800,
})
if (state.disabled) {
active.push({
backgroundColor: t.palette.contrast_500,
})
}
}
if (state.selected) {
active.push({
backgroundColor: t.palette.contrast_800,
})
hover.push({
backgroundColor: t.palette.contrast_800,
})
if (state.disabled) {
base.push({
backgroundColor: t.palette.contrast_100,
})
text.push({
opacity: 0.5,
active.push({
backgroundColor: t.palette.contrast_500,
})
}
}
return {
baseStyles: base,
hoverStyles: hover,
activeStyles: active,
textStyles: text,
}
}, [t, state])
if (state.disabled) {
base.push({
backgroundColor: t.palette.contrast_100,
})
}
return {
baseStyles: base,
hoverStyles: hover,
activeStyles: active,
}
}, [t, state])
return (
<View
@@ -110,19 +102,37 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
activeStyles,
(state.hovered || state.pressed) && hoverStyles,
]}>
{typeof children === 'string' ? (
<Text
style={[
a.text_center,
a.font_bold,
t.atoms.text_contrast_medium,
textStyles,
]}>
{children}
</Text>
) : (
children
)}
{children}
</View>
)
}
export function ButtonText({children}: {children: React.ReactNode}) {
const t = useTheme()
const state = Toggle.useItemContext()
const textStyles = React.useMemo(() => {
const text: TextStyle[] = []
if (state.selected) {
text.push(t.atoms.text_inverted)
}
if (state.disabled) {
text.push({
opacity: 0.5,
})
}
return text
}, [t, state])
return (
<Text
style={[
a.text_center,
a.font_bold,
t.atoms.text_contrast_medium,
textStyles,
]}>
{children}
</Text>
)
}
@@ -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>
</>
+9
View File
@@ -45,10 +45,12 @@ export type LogEvents = {
'onboarding:moderation:nextPressed': {}
'onboarding:finished:nextPressed': {}
'feed:endReached': {
feedUrl: string
feedType: string
itemCount: number
}
'feed:refresh': {
feedUrl: string
feedType: string
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
}
@@ -65,6 +67,10 @@ export type LogEvents = {
logContext: 'Composer'
}
'post:like': {
doesLikerFollowPoster: boolean | undefined
doesPosterFollowLiker: boolean | undefined
likerClout: number | undefined
postClout: number | undefined
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:repost': {
@@ -77,6 +83,9 @@ export type LogEvents = {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'profile:follow': {
didBecomeMutual: boolean | undefined
followeeClout: number | undefined
followerClout: number | undefined
logContext:
| 'RecommendedFollowsItem'
| 'PostThreadItem'
+12
View File
@@ -43,6 +43,14 @@ export function attachRouteToLogEvents(
getCurrentRouteName = getRouteName
}
export function toClout(n: number | null | undefined): number | undefined {
if (n == null) {
return undefined
} else {
return Math.max(0, Math.round(Math.log(n)))
}
}
export function logEvent<E extends keyof LogEvents>(
eventName: E & string,
rawMetadata: LogEvents[E] & FlatJSONRecord,
@@ -78,6 +86,10 @@ function toStatsigUser(did: string | undefined) {
return {
userID,
platform: Platform.OS,
custom: {
// Need to specify here too for gating.
platform: Platform.OS,
},
}
}
+2 -2
View File
@@ -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>
-2
View File
@@ -21,9 +21,7 @@ export const LoginForm = ({
error,
serviceUrl,
serviceDescription,
setError,
setServiceUrl,
onPressRetryConnect,
onPressBack,
}: {
error: string
+2 -2
View File
@@ -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)})
+34 -8
View File
@@ -1,13 +1,14 @@
import {useCallback} from 'react'
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {track} from '#/lib/analytics/analytics'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {Shadow} from '#/state/cache/types'
import {getAgent} from '#/state/session'
import {getAgent, useSession} from '#/state/session'
import {findProfileQueryData} from './profile'
const RQKEY_ROOT = 'post'
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
@@ -68,7 +69,7 @@ export function usePostLikeMutationQueue(
const postUri = post.uri
const postCid = post.cid
const initialLikeUri = post.viewer?.like
const likeMutation = usePostLikeMutation(logContext)
const likeMutation = usePostLikeMutation(logContext, post)
const unlikeMutation = usePostUnlikeMutation(logContext)
const queueToggle = useToggleMutationQueue({
@@ -117,15 +118,40 @@ export function usePostLikeMutationQueue(
return [queueLike, queueUnlike]
}
function usePostLikeMutation(logContext: LogEvents['post:like']['logContext']) {
function usePostLikeMutation(
logContext: LogEvents['post:like']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
) {
const {currentAccount} = useSession()
const queryClient = useQueryClient()
const postAuthor = post.author
return useMutation<
{uri: string}, // responds with the uri of the like
Error,
{uri: string; cid: string} // the post's uri and cid
>({
mutationFn: post => {
logEvent('post:like', {logContext})
return getAgent().like(post.uri, post.cid)
mutationFn: ({uri, cid}) => {
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
if (currentAccount) {
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
}
logEvent('post:like', {
logContext,
doesPosterFollowLiker: postAuthor.viewer
? Boolean(postAuthor.viewer.followedBy)
: undefined,
doesLikerFollowPoster: postAuthor.viewer
? Boolean(postAuthor.viewer.following)
: undefined,
likerClout: toClout(ownProfile?.followersCount),
postClout:
post.likeCount != null &&
post.repostCount != null &&
post.replyCount != null
? toClout(post.likeCount + post.repostCount + post.replyCount)
: undefined,
})
return getAgent().like(uri, cid)
},
onSuccess() {
track('Post:Like')
+26 -3
View File
@@ -20,7 +20,7 @@ import {track} from '#/lib/analytics/analytics'
import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
import {Shadow} from '#/state/cache/types'
import {STALE} from '#/state/queries'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
@@ -202,7 +202,7 @@ export function useProfileFollowMutationQueue(
const queryClient = useQueryClient()
const did = profile.did
const initialFollowingUri = profile.viewer?.following
const followMutation = useProfileFollowMutation(logContext)
const followMutation = useProfileFollowMutation(logContext, profile)
const unfollowMutation = useProfileUnfollowMutation(logContext)
const queueToggle = useToggleMutationQueue({
@@ -252,10 +252,24 @@ export function useProfileFollowMutationQueue(
function useProfileFollowMutation(
logContext: LogEvents['profile:follow']['logContext'],
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
) {
const {currentAccount} = useSession()
const queryClient = useQueryClient()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
logEvent('profile:follow', {logContext})
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
if (currentAccount) {
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
}
logEvent('profile:follow', {
logContext,
didBecomeMutual: profile.viewer
? Boolean(profile.viewer.followedBy)
: undefined,
followeeClout: toClout(profile.followersCount),
followerClout: toClout(ownProfile?.followersCount),
})
return await getAgent().follow(did)
},
onSuccess(data, variables) {
@@ -530,3 +544,12 @@ export function* findAllProfilesInQueryData(
}
}
}
export function findProfileQueryData(
queryClient: QueryClient,
did: string,
): AppBskyActorDefs.ProfileViewDetailed | undefined {
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewDetailed>(
RQKEY(did),
)
}
+14 -7
View File
@@ -1,6 +1,8 @@
import React from 'react'
import * as persisted from '#/state/persisted'
import {useGate} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted'
type StateContext = string
type SetContext = (v: string) => void
@@ -8,7 +10,7 @@ type SetContext = (v: string) => void
const stateContext = React.createContext<StateContext>('home')
const setContext = React.createContext<SetContext>((_: string) => {})
function getInitialFeed() {
function getInitialFeed(startSessionWithFollowing: boolean) {
if (isWeb) {
if (window.location.pathname === '/') {
const params = new URLSearchParams(window.location.search)
@@ -24,16 +26,21 @@ function getInitialFeed() {
return feedFromSession
}
}
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
if (feedFromPersisted) {
// Fall back to the last chosen one across all tabs.
return feedFromPersisted
if (!startSessionWithFollowing) {
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
if (feedFromPersisted) {
// Fall back to the last chosen one across all tabs.
return feedFromPersisted
}
}
return 'home'
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(getInitialFeed)
const startSessionWithFollowing = useGate('start_session_with_following')
const [state, setState] = React.useState(() =>
getInitialFeed(startSessionWithFollowing),
)
const saveState = React.useCallback((feed: string) => {
setState(feed)
+7 -3
View File
@@ -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>
+1 -5
View File
@@ -508,11 +508,7 @@ export const ComposePost = observer(function ComposePost({
title={_(msg`Discard draft?`)}
description={_(msg`Are you sure you'd like to discard this draft?`)}
onConfirm={() => {
if (isWeb) {
onClose()
} else {
discardPromptControl.close(onClose)
}
discardPromptControl.close(onClose)
}}
confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative"
+36 -22
View File
@@ -1,28 +1,29 @@
import React from 'react'
import {useNavigation} from '@react-navigation/native'
import {useAnalytics} from 'lib/analytics/analytics'
import {useQueryClient} from '@tanstack/react-query'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {MainScrollProvider} from '../util/MainScrollProvider'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useSetMinimalShellMode} from '#/state/shell'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {ComposeIcon2} from 'lib/icons'
import {s} from 'lib/styles'
import {View, useWindowDimensions} from 'react-native'
import {ListMethods} from '../util/List'
import {Feed} from '../posts/Feed'
import {FAB} from '../util/fab/FAB'
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
import {useWindowDimensions, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {listenSoftReset} from '#/state/events'
import {truncateAndInvalidate} from '#/state/queries/util'
import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
import {logEvent, useGate} from '#/lib/statsig/statsig'
import {isNative} from '#/platform/detection'
import {logEvent} from '#/lib/statsig/statsig'
import {listenSoftReset} from '#/state/events'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {ComposeIcon2} from 'lib/icons'
import {s} from 'lib/styles'
import {Feed} from '../posts/Feed'
import {FAB} from '../util/fab/FAB'
import {ListMethods} from '../util/List'
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
import {MainScrollProvider} from '../util/MainScrollProvider'
const POLL_FREQ = 60e3 // 60sec
@@ -71,6 +72,7 @@ export function FeedPage({
setHasNew(false)
logEvent('feed:refresh', {
feedType: feed.split('|')[0],
feedUrl: feed,
reason: 'soft-reset',
})
}
@@ -96,10 +98,22 @@ export function FeedPage({
setHasNew(false)
logEvent('feed:refresh', {
feedType: feed.split('|')[0],
feedUrl: feed,
reason: 'load-latest',
})
}, [scrollToTop, feed, queryClient, setHasNew])
let feedPollInterval
if (
useGate('disable_poll_on_discover') &&
feed === // Discover
'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
) {
feedPollInterval = undefined
} else {
feedPollInterval = POLL_FREQ
}
return (
<View testID={testID} style={s.h100pct}>
<MainScrollProvider>
@@ -108,7 +122,7 @@ export function FeedPage({
enabled={isPageFocused}
feed={feed}
feedParams={feedParams}
pollInterval={POLL_FREQ}
pollInterval={feedPollInterval}
disablePoll={hasNew}
scrollElRef={scrollElRef}
onScrolledDownChange={setIsScrolledDown}
@@ -1,24 +1,25 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {AppBskyActorDefs} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {Text} from 'view/com/util/text/Text'
import * as Toast from 'view/com/util/Toast'
import {s} from 'lib/styles'
import {track} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles'
import {Shadow, useProfileShadow} from 'state/cache/profile-shadow'
import {track} from 'lib/analytics/analytics'
import {
useProfileFollowMutationQueue,
useProfileQuery,
} from 'state/queries/profile'
import {useRequireAuth} from 'state/session'
import {Text} from 'view/com/util/text/Text'
import * as Toast from 'view/com/util/Toast'
export function PostThreadFollowBtn({did}: {did: string}) {
const {data: profile, isLoading} = useProfileQuery({did})
@@ -47,8 +48,10 @@ function PostThreadFollowBtnLoaded({
'PostThreadItem',
)
const requireAuth = useRequireAuth()
const showFollowBackLabel = useGate('show_follow_back_label')
const isFollowing = !!profile.viewer?.following
const isFollowedBy = !!profile.viewer?.followedBy
const [wasFollowing, setWasFollowing] = React.useState<boolean>(isFollowing)
// This prevents the button from disappearing as soon as we follow.
@@ -136,7 +139,15 @@ function PostThreadFollowBtnLoaded({
type="button"
style={[!isFollowing ? palInverted.text : pal.text, s.bold]}
numberOfLines={1}>
{!isFollowing ? <Trans>Follow</Trans> : <Trans>Following</Trans>}
{!isFollowing ? (
showFollowBackLabel && isFollowedBy ? (
<Trans>Follow Back</Trans>
) : (
<Trans>Follow</Trans>
)
) : (
<Trans>Following</Trans>
)}
</Text>
</TouchableOpacity>
</View>
+27 -23
View File
@@ -8,32 +8,33 @@ import {
View,
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {List, ListRef} from '../util/List'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {useAnalytics} from 'lib/analytics/analytics'
import {useTheme} from 'lib/ThemeContext'
import {logger} from '#/logger'
import {
RQKEY,
FeedDescriptor,
FeedParams,
usePostFeedQuery,
pollLatest,
} from '#/state/queries/post-feed'
import {isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events'
import {useSession} from '#/state/session'
import {STALE} from '#/state/queries'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {useQueryClient} from '@tanstack/react-query'
import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events'
import {STALE} from '#/state/queries'
import {
FeedDescriptor,
FeedParams,
pollLatest,
RQKEY,
usePostFeedQuery,
} from '#/state/queries/post-feed'
import {useSession} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {useTheme} from 'lib/ThemeContext'
import {List, ListRef} from '../util/List'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedSlice} from './FeedSlice'
const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -217,6 +218,7 @@ let Feed = ({
track('Feed:onRefresh')
logEvent('feed:refresh', {
feedType: feedType,
feedUrl: feed,
reason: 'pull-to-refresh',
})
setIsPTRing(true)
@@ -227,13 +229,14 @@ let Feed = ({
logger.error('Failed to refresh posts feed', {message: err})
}
setIsPTRing(false)
}, [refetch, track, setIsPTRing, onHasNew, feedType])
}, [refetch, track, setIsPTRing, onHasNew, feed, feedType])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
logEvent('feed:endReached', {
feedType: feedType,
feedUrl: feed,
itemCount: feedItems.length,
})
track('Feed:onEndReached')
@@ -248,6 +251,7 @@ let Feed = ({
isError,
fetchNextPage,
track,
feed,
feedType,
feedItems.length,
])
+7 -7
View File
@@ -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
View File
@@ -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>
+8 -8
View File
@@ -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>
),
+9 -9
View File
@@ -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>
+8 -8
View File
@@ -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>
+16 -17
View File
@@ -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>
+4 -4
View File
@@ -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"