Compare commits

...

11 Commits

Author SHA1 Message Date
Eric Bailey 8f16625998 Allow ternary 2026-01-16 16:11:25 -06:00
Eric Bailey 5622d522cf Organize a bit, add quiet to main lint command 2026-01-16 16:06:30 -06:00
Eric Bailey 1741e6fc30 Remove unused globals? 2026-01-16 15:59:27 -06:00
Eric Bailey be0e100049 Fix CI failure 2026-01-16 15:55:18 -06:00
Samuel Newman 8a50e5553b fix yarn lock ci 2026-01-15 16:09:26 +02:00
Samuel Newman 6a0be039e9 enable typechecked rules, switch them to warn 2026-01-15 15:24:52 +02:00
Samuel Newman d0597f6a40 lint android a11y 2026-01-15 10:51:55 +02:00
Samuel Newman dfaefb6ceb update eslint package versions 2026-01-15 10:51:55 +02:00
Claude 9481e24b82 Update ESLint rule tests for flat config format
- Update RuleTester to use flat config languageOptions instead of
  eslintrc parser format
- Remove duplicate test case that ESLint v9 now detects
- Add Jest globals for test files
2026-01-15 10:51:32 +02:00
Claude b88a3a5579 Fix varsIgnorePattern to require character after underscore
Restore the original pattern `^_.+` instead of `^_` so that lingui's
`const { _ } = useLingui()` will still be flagged when unused.
2026-01-15 10:51:32 +02:00
Claude 0cd99c841e Upgrade ESLint to v9 with flat config
- Upgrade eslint from v8 to v9.18.0
- Migrate from .eslintrc.js to eslint.config.mjs (flat config)
- Upgrade typescript-eslint to v8.20.0 (unified package)
- Replace eslint-plugin-import with eslint-plugin-import-x for flat config support
- Add globals package for environment globals
- Update eslint-plugin-bsky-internal with proper meta objects for ESLint v9
- Fix deprecated context.getScope() API usage
- Update bskyembed to use flat config
- Remove deprecated --ext flag from lint scripts
- Configure rules to maintain previous behavior while using new ESLint version
2026-01-15 10:51:32 +02:00
38 changed files with 1914 additions and 2139 deletions
-127
View File
@@ -1,127 +0,0 @@
module.exports = {
root: true,
extends: [
'@react-native',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-native-a11y/ios',
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'react',
'lingui',
'simple-import-sort',
'bsky-internal',
'eslint-plugin-react-compiler',
'import',
],
rules: {
'react/no-unescaped-entities': 0,
'react/prop-types': 0,
'react-native/no-inline-styles': 0,
'bsky-internal/avoid-unwrapped-text': [
'error',
{
impliedTextComponents: [
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'P',
'Admonition',
'Admonition.Admonition',
'Toast.Action',
'toast.Action',
'AgeAssuranceAdmonition',
'Span',
'StackedButton',
],
impliedTextProps: [],
suggestedTextWrappers: {
Button: 'ButtonText',
'ToggleButton.Button': 'ToggleButton.ButtonText',
'SegmentedControl.Item': 'SegmentedControl.ItemText',
},
},
],
'bsky-internal/use-exact-imports': 'error',
'bsky-internal/use-typed-gates': 'error',
'bsky-internal/use-prefixed-imports': 'error',
'simple-import-sort/imports': [
'error',
{
groups: [
// Side effect imports.
['^\\u0000'],
// Node.js builtins prefixed with `node:`.
['^node:'],
// Packages.
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
// React/React Native priortized, followed by expo
// Followed by all packages excluding unprefixed relative ones
[
'^(react\\/(.*)$)|^(react$)|^(react-native(.*)$)',
'^(expo(.*)$)|^(expo$)',
'^(?!(?:alf|components|lib|locale|logger|platform|screens|state|view)(?:$|\\/))@?\\w',
],
// Relative imports.
// Ideally, anything that starts with a dot or #
// due to unprefixed relative imports being used, we whitelist the relative paths we use
// (?:$|\\/) matches end of string or /
[
'^(?:#\\/)?(?:lib|state|logger|platform|locale)(?:$|\\/)',
'^(?:#\\/)?view(?:$|\\/)',
'^(?:#\\/)?screens(?:$|\\/)',
'^(?:#\\/)?alf(?:$|\\/)',
'^(?:#\\/)?components(?:$|\\/)',
'^#\\/',
'^\\.',
],
// anything else - hopefully we don't have any of these
['^'],
],
},
],
'simple-import-sort/exports': 'error',
'react-compiler/react-compiler': 'warn',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{argsIgnorePattern: '^_', varsIgnorePattern: '^_.+'},
],
'@typescript-eslint/consistent-type-imports': [
'warn',
{prefer: 'type-imports', fixStyle: 'inline-type-imports'},
],
'import/consistent-type-specifier-style': ['warn', 'prefer-inline'],
},
ignorePatterns: [
'**/__mocks__/*.ts',
'src/platform/polyfills.ts',
'src/third-party',
'ios',
'android',
'coverage',
'*.lock',
'.husky',
'patches',
'*.html',
'bskyweb',
'bskyembed',
'src/locale/locales/_build/',
'src/locale/locales/**/*.js',
'*.e2e.ts',
'*.e2e.tsx',
],
settings: {
componentWrapperFunctions: ['observer'],
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
},
}
-22
View File
@@ -1,22 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'simple-import-sort'],
extends: [
'eslint:recommended',
'preact',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
rules: {
'simple-import-sort/imports': 'warn',
'simple-import-sort/exports': 'warn',
'no-else-return': 'off',
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
}
+52
View File
@@ -0,0 +1,52 @@
// @ts-check
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import simpleImportSort from 'eslint-plugin-simple-import-sort'
import globals from 'globals'
export default tseslint.config(
// Global ignores
{
ignores: ['dist/**', 'node_modules/**'],
},
// Base JS recommended rules
js.configs.recommended,
// TypeScript rules with type checking
...tseslint.configs.recommendedTypeChecked,
// Main configuration
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: {
'simple-import-sort': simpleImportSort,
},
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
},
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'simple-import-sort/imports': 'warn',
'simple-import-sort/exports': 'warn',
'no-else-return': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_.+',
caughtErrors: 'none',
ignoreRestSiblings: true,
},
],
},
},
)
+6 -4
View File
@@ -7,7 +7,7 @@
"dev-snippet": "tsc --project tsconfig.snippet.json && serve -s dist -p 3000 -n",
"build": "tsc && vite build",
"build-snippet": "tsc --project tsconfig.snippet.json",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
"lint": "eslint --cache src",
"typecheck": "tsc --noEmit"
},
"dependencies": {
@@ -17,11 +17,13 @@
"devDependencies": {
"@preact/preset-vite": "^2.10.2",
"@vitejs/plugin-legacy": "^7.0.0",
"@eslint/js": "^9.18.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.19.0",
"eslint-config-preact": "^1.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint": "^9.18.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^15.14.0",
"postcss": "^8.4.38",
"typescript-eslint": "^8.20.0",
"serve": "^14.2.5",
"tailwindcss": "^3.4.3",
"terser": "^5.43.1",
+305 -1347
View File
File diff suppressed because it is too large Load Diff
+271
View File
@@ -0,0 +1,271 @@
// @ts-check
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import { defineConfig } from 'eslint/config';
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
// @ts-expect-error no types
import reactNative from 'eslint-plugin-react-native'
// @ts-expect-error no types
import reactNativeA11y from 'eslint-plugin-react-native-a11y'
import simpleImportSort from 'eslint-plugin-simple-import-sort'
import importX from 'eslint-plugin-import-x'
import lingui from 'eslint-plugin-lingui'
import reactCompiler from 'eslint-plugin-react-compiler'
import bskyInternal from 'eslint-plugin-bsky-internal'
import globals from 'globals'
import tsParser from '@typescript-eslint/parser'
export default defineConfig(
/**
* Global ignores
*/
{
ignores: [
'**/__mocks__/*.ts',
'src/platform/polyfills.ts',
'src/third-party/**',
'ios/**',
'android/**',
'coverage/**',
'*.lock',
'.husky/**',
'patches/**',
'*.html',
'bskyweb/**',
'bskyembed/**',
'src/locale/locales/_build/**',
'src/locale/locales/**/*.js',
'*.e2e.ts',
'*.e2e.tsx',
'eslint.config.mjs',
],
},
/**
* Base configurations
*/
js.configs.recommended,
tseslint.configs.recommendedTypeChecked,
reactHooks.configs.flat.recommended,
// @ts-expect-error https://github.com/un-ts/eslint-plugin-import-x/issues/439
importX.flatConfigs.recommended,
importX.flatConfigs.typescript,
importX.flatConfigs['react-native'],
/**
* Main configuration for all JS/TS/JSX/TSX files
*/
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: {
react,
'react-native': reactNative,
'react-native-a11y': reactNativeA11y,
'simple-import-sort': simpleImportSort,
lingui,
'react-compiler': reactCompiler,
'bsky-internal': bskyInternal,
},
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
},
parserOptions: {
parser: tsParser,
projectService: true,
ecmaFeatures: {
jsx: true,
},
},
},
settings: {
react: {
version: 'detect',
},
componentWrapperFunctions: ['observer'],
},
rules: {
/**
* Custom rules
*/
'bsky-internal/avoid-unwrapped-text': [
'error',
{
impliedTextComponents: [
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'P',
'Admonition',
'Admonition.Admonition',
'Toast.Action',
'AgeAssuranceAdmonition',
'Span',
'StackedButton',
],
impliedTextProps: [],
suggestedTextWrappers: {
Button: 'ButtonText',
'ToggleButton.Button': 'ToggleButton.ButtonText',
'SegmentedControl.Item': 'SegmentedControl.ItemText',
},
},
],
'bsky-internal/use-exact-imports': 'error',
'bsky-internal/use-typed-gates': 'error',
'bsky-internal/use-prefixed-imports': 'error',
/**
* React & React Native
*/
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
'react/no-unescaped-entities': 'off',
'react/prop-types': 'off',
'react-native/no-inline-styles': 'off',
...reactNativeA11y.configs.all.rules,
'react-compiler/react-compiler': 'warn',
// TODO: Fix these and set to error
'react-hooks/set-state-in-effect': 'warn',
'react-hooks/purity': 'warn',
'react-hooks/refs': 'warn',
'react-hooks/immutability': 'warn',
/**
* Import sorting
*/
'simple-import-sort/imports': [
'error',
{
groups: [
// Side effect imports.
['^\\u0000'],
// Node.js builtins prefixed with `node:`.
['^node:'],
// Packages.
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
// React/React Native prioritized, followed by expo
// Followed by all packages excluding unprefixed relative ones
[
'^(react\\/(.*)$)|^(react$)|^(react-native(.*)$)',
'^(expo(.*)$)|^(expo$)',
'^(?!(?:alf|components|lib|locale|logger|platform|screens|state|view)(?:$|\\/))@?\\w',
],
// Relative imports.
// Ideally, anything that starts with a dot or #
// due to unprefixed relative imports being used, we whitelist the relative paths we use
// (?:$|\\/) matches end of string or /
[
'^(?:#\\/)?(?:lib|state|logger|platform|locale)(?:$|\\/)',
'^(?:#\\/)?view(?:$|\\/)',
'^(?:#\\/)?screens(?:$|\\/)',
'^(?:#\\/)?alf(?:$|\\/)',
'^(?:#\\/)?components(?:$|\\/)',
'^#\\/',
'^\\.',
],
// anything else - hopefully we don't have any of these
['^'],
],
},
],
'simple-import-sort/exports': 'error',
/**
* Import linting
*/
'import-x/consistent-type-specifier-style': ['warn', 'prefer-inline'],
'import-x/no-unresolved': ['error', {
/*
* The `postinstall` hook runs `compile-if-needed` locally, but not in
* CI. For CI-sake, ignore this.
*/
ignore: ['^#\/locale\/locales\/.+\/messages'],
}],
/**
* TypeScript-specific rules
*/
'no-unused-vars': 'off', // off, we use TS-specific rule below
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_.+',
caughtErrors: 'none',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/consistent-type-imports': [
'warn',
{prefer: 'type-imports', fixStyle: 'inline-type-imports'},
],
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unused-expressions': ['error', {
allowTernary: true,
}],
/**
* Maintain previous behavior - these are stricter in typescript-eslint
* v8 `warn` ones are probably worth fixing. `off` ones are a bit too
* nit-picky
*/
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-misused-promises': 'warn',
'@typescript-eslint/require-await': 'warn',
'@typescript-eslint/no-unsafe-enum-comparison': 'warn',
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
'@typescript-eslint/no-redundant-type-constituents': 'warn',
'@typescript-eslint/no-duplicate-type-constituents': 'warn',
'@typescript-eslint/no-base-to-string': 'warn',
'@typescript-eslint/prefer-promise-reject-errors': 'warn',
'@typescript-eslint/await-thenable': 'warn',
/**
* Turn off rules that we haven't enforced thus far
*/
'no-empty-pattern': 'off',
'no-async-promise-executor': 'off',
'no-constant-binary-expression': 'warn',
'prefer-const': 'off',
'no-empty': 'off',
'no-unsafe-optional-chaining': 'off',
'no-prototype-builtins': 'off',
'no-var': 'off',
'prefer-rest-params': 'off',
'no-case-declarations': 'off',
'no-irregular-whitespace': 'off',
'no-useless-escape': 'off',
'no-sparse-arrays': 'off',
'no-fallthrough': 'off',
'no-control-regex': 'off',
},
},
/**
* Test files configuration
*/
{
files: ['**/__tests__/**/*.{js,jsx,ts,tsx}', '**/*.test.{js,jsx,ts,tsx}'],
languageOptions: {
globals: {
...globals.jest,
}
},
},
)
+9 -15
View File
@@ -1,14 +1,17 @@
const {RuleTester} = require('eslint')
const tseslint = require('typescript-eslint')
const avoidUnwrappedText = require('../avoid-unwrapped-text')
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaFeatures: {
jsx: true,
languageOptions: {
parser: tseslint.parser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
ecmaVersion: 6,
sourceType: 'module',
},
})
@@ -773,15 +776,6 @@ function MyText({ foo }) {
errors: 1,
},
{
code: `
<View>
<Trans>{'foo'}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View prop={
+305 -277
View File
@@ -29,303 +29,331 @@ function getTagName(node) {
return reversedIdentifiers.reverse().join('.')
}
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]
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce text strings are wrapped in <Text> components',
},
schema: [
{
type: 'object',
properties: {
impliedTextComponents: {
type: 'array',
items: {type: 'string'},
},
impliedTextProps: {
type: 'array',
items: {type: 'string'},
},
suggestedTextWrappers: {
type: 'object',
additionalProperties: {type: 'string'},
},
},
additionalProperties: false,
},
],
},
create(context) {
const options = context.options[0] || {}
const impliedTextProps = options.impliedTextProps ?? []
const impliedTextComponents = options.impliedTextComponents ?? []
const suggestedTextWrappers = options.suggestedTextWrappers ?? {}
const textProps = [...impliedTextProps]
const textComponents = ['Text', ...impliedTextComponents]
function isTextComponent(tagName) {
return textComponents.includes(tagName) || tagName.endsWith('Text')
}
function isTextComponent(tagName) {
return textComponents.includes(tagName) || tagName.endsWith('Text')
}
return {
JSXText(node) {
if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) {
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 {
JSXText(node) {
if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) {
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 === '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')
parent.type === 'JSXAttribute' &&
parent.name.type === 'JSXIdentifier' &&
parent.parent.type === 'JSXOpeningElement' &&
parent.parent.parent.type === 'JSXElement'
) {
// We're good.
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 string 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
}
const message =
'Wrap this string 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,
})
parent = parent.parent
continue
}
},
Literal(node) {
if (typeof node.value !== 'string' && typeof node.value !== 'number') {
return
}
parent = parent.parent
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.
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 (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 (parent.type === 'BinaryExpression' && parent.operator === '+') {
parent = parent.parent
continue
}
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')
parent.type === 'JSXExpressionContainer' ||
parent.type === 'LogicalExpression'
) {
// We're good.
return
parent = parent.parent
continue
}
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,
})
// 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
}
parent = parent.parent
continue
}
},
ReturnStatement(node) {
let fnScope = context.getScope()
while (fnScope && fnScope.type !== 'function') {
fnScope = fnScope.upper
}
if (!fnScope) {
return
}
const fn = fnScope.block
if (!fn.id || fn.id.type !== 'Identifier' || !fn.id.name) {
return
}
if (!/^[A-Z]\w*Text$/.test(fn.id.name)) {
return
}
if (!node.argument || node.argument.type !== 'JSXElement') {
return
}
const openingEl = node.argument.openingElement
if (openingEl.name.type !== 'JSXIdentifier') {
return
}
const returnedComponentName = openingEl.name.name
if (!isTextComponent(returnedComponentName)) {
context.report({
node,
message:
'Components ending with *Text must return <Text> or <SomeText>.',
})
}
},
}
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.sourceCode.getScope(node)
while (fnScope && fnScope.type !== 'function') {
fnScope = fnScope.upper
}
if (!fnScope) {
return
}
const fn = fnScope.block
if (!fn.id || fn.id.type !== 'Identifier' || !fn.id.name) {
return
}
if (!/^[A-Z]\w*Text$/.test(fn.id.name)) {
return
}
if (!node.argument || node.argument.type !== 'JSXElement') {
return
}
const openingEl = node.argument.openingElement
if (openingEl.name.type !== 'JSXIdentifier') {
return
}
const returnedComponentName = openingEl.name.name
if (!isTextComponent(returnedComponentName)) {
context.report({
node,
message:
'Components ending with *Text must return <Text> or <SomeText>.',
})
}
},
}
},
}
+7 -1
View File
@@ -1,6 +1,10 @@
'use strict'
module.exports = {
const plugin = {
meta: {
name: 'eslint-plugin-bsky-internal',
version: '1.0.0',
},
rules: {
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
'use-exact-imports': require('./use-exact-imports'),
@@ -8,3 +12,5 @@ module.exports = {
'use-prefixed-imports': require('./use-prefixed-imports'),
},
}
module.exports = plugin
+24 -15
View File
@@ -3,20 +3,29 @@ const BANNED_IMPORTS = [
'@fortawesome/free-solid-svg-icons',
]
exports.create = function create(context) {
return {
ImportDeclaration(node) {
const source = node.source
if (typeof source.value !== 'string') {
return
}
if (BANNED_IMPORTS.includes(source.value)) {
context.report({
node,
message:
'Import the specific thing you want instead of the entire package',
})
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Prevent importing entire icon packages',
},
}
schema: [],
},
create(context) {
return {
ImportDeclaration(node) {
const source = node.source
if (typeof source.value !== 'string') {
return
}
if (BANNED_IMPORTS.includes(source.value)) {
context.report({
node,
message:
'Import the specific thing you want instead of the entire package',
})
}
},
}
},
}
+4
View File
@@ -13,7 +13,11 @@ const BANNED_IMPORT_PREFIXES = [
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Enforce using prefixed imports for internal paths',
},
fixable: 'code',
schema: [],
},
create(context) {
return {
+37 -27
View File
@@ -1,31 +1,41 @@
'use strict'
exports.create = function create(context) {
return {
ImportSpecifier(node) {
if (
!node.local ||
node.local.type !== 'Identifier' ||
node.local.name !== 'useGate'
) {
return
}
if (
node.parent.type !== 'ImportDeclaration' ||
!node.parent.source ||
node.parent.source.type !== 'Literal'
) {
return
}
const source = node.parent.source.value
if (source.startsWith('statsig') || source.startsWith('@statsig')) {
context.report({
node,
message:
"Use useGate() from '#/lib/statsig/statsig' instead of the one on npm.",
})
}
// TODO: Verify gate() call results aren't stored in variables.
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Enforce using internal statsig wrapper instead of npm package',
},
}
schema: [],
},
create(context) {
return {
ImportSpecifier(node) {
if (
!node.local ||
node.local.type !== 'Identifier' ||
node.local.name !== 'useGate'
) {
return
}
if (
node.parent.type !== 'ImportDeclaration' ||
!node.parent.source ||
node.parent.source.type !== 'Literal'
) {
return
}
const source = node.parent.source.value
if (source.startsWith('statsig') || source.startsWith('@statsig')) {
context.report({
node,
message:
"Use useGate() from '#/lib/statsig/statsig' instead of the one on npm.",
})
}
// TODO: Verify gate() call results aren't stored in variables.
},
}
},
}
+13 -11
View File
@@ -41,7 +41,7 @@
"test-watch": "NODE_ENV=test jest --watchAll",
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "NODE_ENV=test jest --coverage",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
"lint": "eslint --cache --quiet src",
"lint-native": "swiftlint ./modules && ktlint ./modules",
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
"typecheck": "tsc --project ./tsconfig.check.json",
@@ -231,6 +231,7 @@
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
"@eslint/js": "^9.39.2",
"@expo/config-plugins": "~54.0.1",
"@lingui/cli": "^4.14.1",
"@lingui/macro": "^4.14.1",
@@ -248,23 +249,24 @@
"@types/psl": "^1.1.1",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"babel-jest": "^29.7.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-react-compiler": "^19.1.0-rc.3",
"babel-preset-expo": "~54.0.0",
"eslint": "^8.19.0",
"eslint": "^9.39.2",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-bsky-internal": "link:./eslint",
"eslint-plugin-ft-flow": "^2.0.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-lingui": "^0.2.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-lingui": "^0.11.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-native": "^5.0.0",
"eslint-plugin-react-native-a11y": "^3.5.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
"file-loader": "6.2.0",
"globals": "^17.0.0",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^29.7.0",
@@ -279,6 +281,7 @@
"ts-node": "^10.9.1",
"ts-plugin-sort-import-suggestions": "^1.0.4",
"typescript": "^5.9.2",
"typescript-eslint": "^8.53.0",
"webpack-bundle-analyzer": "^4.10.1"
},
"resolutions": {
@@ -288,7 +291,6 @@
"**/@react-native-async-storage/async-storage": "2.2.0",
"**/expo-constants": "18.0.8",
"**/expo-device": "7.1.4",
"**/zod": "3.23.8",
"**/multiformats": "9.9.0",
"unicode-segmenter": "0.14.5"
},
@@ -215,18 +215,18 @@ export function Controls({
const seekLeft = useCallback(() => {
if (!videoRef.current) return
// eslint-disable-next-line @typescript-eslint/no-shadow
const currentTime = videoRef.current.currentTime
// eslint-disable-next-line @typescript-eslint/no-shadow
const duration = videoRef.current.duration || 0
onSeek(clamp(currentTime - 5, 0, duration))
}, [onSeek, videoRef])
const seekRight = useCallback(() => {
if (!videoRef.current) return
// eslint-disable-next-line @typescript-eslint/no-shadow
const currentTime = videoRef.current.currentTime
// eslint-disable-next-line @typescript-eslint/no-shadow
const duration = videoRef.current.duration || 0
onSeek(clamp(currentTime + 5, 0, duration))
}, [onSeek, videoRef])
+1
View File
@@ -35,6 +35,7 @@ export function Text({
if (__DEV__) {
if (!emoji && childHasEmoji(children)) {
logger.warn(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
)
}
+1 -1
View File
@@ -98,7 +98,7 @@ function DialogInner({
} = useRemoveLiveStatusMutation()
const {minutesUntilExpiry, expiryDateTime} = useMemo(() => {
tick!
void tick
const expiry = new Date(status.expiresAt ?? new Date())
return {
+6 -3
View File
@@ -13,7 +13,11 @@ import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import {getLiveServiceNames} from '#/components/live/utils'
import {
displayDuration,
getLiveServiceNames,
useDebouncedValue,
} from '#/components/live/utils'
import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import * as Select from '#/components/Select'
@@ -21,7 +25,6 @@ import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
import {LinkPreview} from './LinkPreview'
import {useLiveLinkMetaQuery, useUpsertLiveStatusMutation} from './queries'
import {displayDuration, useDebouncedValue} from './utils'
export function GoLiveDialog({
control,
@@ -57,7 +60,7 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
const time = useCallback(
(offset: number) => {
tick!
void tick
const date = new Date()
date.setMinutes(date.getMinutes() + offset)
@@ -211,8 +211,8 @@ function Inner(props: ReportDialogProps) {
logger.metric(
'reportDialog:success',
{
reason: state.selectedOption?.reason!,
labeler: state.selectedLabeler?.creator.handle!,
reason: state.selectedOption?.reason ?? '',
labeler: state.selectedLabeler?.creator.handle ?? '',
details: !!state.details,
},
{statsig: false},
@@ -9,7 +9,7 @@ import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader'
import * as toast from '#/components/Toast'
import * as Toast from '#/components/Toast'
import {Span, Text} from '#/components/Typography'
import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences'
import {
@@ -61,14 +61,14 @@ function Inner({
feed,
metricContext,
onUpdateSuccess({undoAction}) {
toast.show(
<toast.Outer>
<toast.Icon />
<toast.Text>
Toast.show(
<Toast.Outer>
<Toast.Icon />
<Toast.Text>
<Trans>Your live event preferences have been updated.</Trans>
</toast.Text>
</Toast.Text>
{undoAction && (
<toast.Action
<Toast.Action
label={_(msg`Undo`)}
onPress={() => {
if (undoAction) {
@@ -76,12 +76,10 @@ function Inner({
}
}}>
<Trans>Undo</Trans>
</toast.Action>
</Toast.Action>
)}
</toast.Outer>,
{
type: 'success',
},
</Toast.Outer>,
{type: 'success'},
)
/*
+1 -1
View File
@@ -128,7 +128,7 @@ export function useGeolocationServiceResponse() {
useEffect(() => {
return onGeolocationServiceResponseUpdate(config => {
setConfig(config!)
setConfig(config)
})
}, [])
+1 -1
View File
@@ -17,7 +17,7 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
const config = useLiveNowConfig()
return useMemo(() => {
tick! // revalidate every minute
void tick // revalidate every minute
if (shadowed && 'status' in shadowed && shadowed.status) {
const isValid = validateStatus(shadowed.status, config)
+1 -1
View File
@@ -67,7 +67,7 @@ export function isPlainArray(value: unknown) {
}
// Copied from: https://github.com/jonschlinkert/is-plain-object
export function isPlainObject(o: any): o is Object {
export function isPlainObject(o: any): o is object {
if (!hasObjectPrototype(o)) {
return false
}
+1 -1
View File
@@ -3,13 +3,13 @@ import {
getInfoAsync,
readDirectoryAsync,
} from 'expo-file-system/legacy'
import {type ImagePickerResult} from 'expo-image-picker'
import ExpoImageCropTool, {
type OpenCropperOptions,
} from '@bsky.app/expo-image-crop-tool'
import {compressIfNeeded} from './manip'
import {type PickerImage} from './picker.shared'
import {ImagePickerResult} from 'expo-image-picker'
async function getFile() {
const imagesDir = documentDirectory!
@@ -138,11 +138,10 @@ function ChatListItemReady({
const {lastMessage, lastMessageSentAt, latestReportableMessage} =
useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-shadow
let lastMessage = _(msg`No messages yet`)
// eslint-disable-next-line @typescript-eslint/no-shadow
let lastMessageSentAt: string | null = null
// eslint-disable-next-line @typescript-eslint/no-shadow
let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
-1
View File
@@ -64,7 +64,6 @@ export function PostThread({uri}: {uri: string}) {
*/
const thread = usePostThread({anchor: uri})
const {anchor, hasParents} = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-shadow
let hasParents = false
for (const item of thread.data.items) {
if (item.type === 'threadPost' && item.depth === 0) {
+1 -1
View File
@@ -95,7 +95,7 @@ export class Convo {
this.convoId = params.convoId
this.agent = params.agent
this.events = params.events
this.senderUserDid = params.agent.session?.did!
this.senderUserDid = params.agent.assertDid
if (params.placeholderData) {
this.setupPlaceholderData(params.placeholderData)
+5
View File
@@ -22,6 +22,8 @@ const UPDATE_EVENT = 'BSKY_UPDATE'
let _state: Schema = defaults
const _emitter = new EventEmitter()
// async, to match native implementation
// eslint-disable-next-line @typescript-eslint/require-await
export async function init() {
broadcast.onmessage = onBroadcastMessage
window.onstorage = onStorage
@@ -37,6 +39,7 @@ export function get<K extends keyof Schema>(key: K): Schema[K] {
}
get satisfies PersistedApi['get']
// eslint-disable-next-line @typescript-eslint/require-await
export async function write<K extends keyof Schema>(
key: K,
value: Schema[K],
@@ -82,6 +85,7 @@ export function onUpdate<K extends keyof Schema>(
}
onUpdate satisfies PersistedApi['onUpdate']
// eslint-disable-next-line @typescript-eslint/require-await
export async function clearStorage() {
try {
localStorage.removeItem(BSKY_STORAGE)
@@ -102,6 +106,7 @@ function onStorage() {
}
}
// eslint-disable-next-line @typescript-eslint/require-await
async function onBroadcastMessage({data}: MessageEvent) {
if (
typeof data === 'object' &&
+1 -1
View File
@@ -23,7 +23,7 @@ export {
useExternalEmbedsPrefs,
useSetExternalEmbedPref,
} from './external-embeds-prefs'
export * from './hidden-posts'
export {useHiddenPosts, useHiddenPostsApi} from './hidden-posts'
export {useLabelDefinitions} from './label-defs'
export {useLanguagePrefs, useLanguagePrefsApi} from './languages'
export {useSetSubtitlesEnabled, useSubtitlesEnabled} from './subtitles'
+7 -1
View File
@@ -15,6 +15,12 @@ export type UsePreferencesQueryResponse = Omit<
}
export type ThreadViewPreferences = {
sort: 'hotness' | 'oldest' | 'newest' | 'most-likes' | 'random' | string
sort:
| 'hotness'
| 'oldest'
| 'newest'
| 'most-likes'
| 'random'
| (string & {})
lab_treeViewEnabled?: boolean
}
@@ -1,4 +1,3 @@
/* eslint-disable no-labels */
import {AppBskyUnspeccedDefs, type ModerationOpts} from '@atproto/api'
import {
+1 -1
View File
@@ -26,7 +26,7 @@ type Controls = {
/**
* The did of the account to populate the login form with.
*/
requestedAccount?: string | 'none' | 'new' | 'starterpack'
requestedAccount?: (string & {}) | 'none' | 'new' | 'starterpack'
}) => void
/**
* Clears the requested account so that next time the logged out view is
@@ -14,7 +14,9 @@ import {
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import PasteInput, {
type PastedFile,
type PasteInputRef, // @ts-expect-error no types when installing from github
type PasteInputRef,
// @ts-expect-error no types when installing from github
// eslint-disable-next-line import-x/no-unresolved
} from '@mattermost/react-native-paste-input'
import {POST_IMG_MAX} from '#/lib/constants'
-1
View File
@@ -437,7 +437,6 @@ let PostFeed = ({
for (const page of data.pages) {
for (const slice of page.slices) {
const item = slice.items.find(
// eslint-disable-next-line @typescript-eslint/no-shadow
item => item.uri === slice.feedPostUri,
)
if (
+4 -2
View File
@@ -1,9 +1,9 @@
import {useState} from 'react'
import {LogBox, Pressable, View, TextInput} from 'react-native'
import {LogBox, Pressable, TextInput, View} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {BLUESKY_PROXY_HEADER} from '#/lib/constants'
import {useSessionApi, useAgent} from '#/state/session'
import {useAgent, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useOnboardingDispatch} from '#/state/shell/onboarding'
import {navigate} from '../../../Navigation'
@@ -50,6 +50,8 @@ export function TestCtrls() {
return (
<View style={{position: 'absolute', top: 100, right: 0, zIndex: 100}}>
<TextInput
accessibilityLabel="Text input field"
accessibilityHint="Enter proxy header"
testID="e2eProxyHeaderInput"
onChangeText={val => setProxyHeader(val as any)}
onSubmitEditing={() => {
-2
View File
@@ -3,12 +3,10 @@ import {type AlertButton, type AlertStatic} from 'react-native'
class WebAlert implements Pick<AlertStatic, 'alert'> {
public alert(title: string, message?: string, buttons?: AlertButton[]): void {
if (buttons === undefined || buttons.length === 0) {
// eslint-disable-next-line no-alert
window.alert([title, message].filter(Boolean).join('\n'))
return
}
// eslint-disable-next-line no-alert
const result = window.confirm([title, message].filter(Boolean).join('\n'))
if (result === true) {
+2 -2
View File
@@ -153,9 +153,9 @@ export function Button({
async (event: GestureResponderEvent) => {
event.stopPropagation()
event.preventDefault()
withLoading && setIsLoading(true)
if (withLoading) setIsLoading(true)
await onPress?.(event)
withLoading && setIsLoading(false)
if (withLoading) setIsLoading(false)
},
[onPress, withLoading],
)
+1
View File
@@ -51,6 +51,7 @@ function Text_DEPRECATED({
if (__DEV__) {
if (!emoji && childHasEmoji(children)) {
logger.warn(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
)
}
+826 -250
View File
File diff suppressed because it is too large Load Diff