Merge remote-tracking branch 'origin/main' into dm-composer

* origin/main: (32 commits)
  Don't call getProfiles if there are no actors (#10196)
  Update browserslist (#10195)
  Fix layout shift when liking a post on Android (#10190)
  Revert PostThread parent chunk size for native (#10193)
  Nightly source-language update
  Bump expo-paste-input (#10187)
  Add some more clarity to the RQ docs (#10120)
  [APP-2038] fix feeds not refreshing (#10167)
  [APP-1999] Fix ReportDialog Android hang by narrowing dependency arrays (#10171)
  Minor hotkeys nit (#10184)
  Fixes Hotkeys causing native to crash immediately on start (#10180)
  Nightly source-language update
  Fix OOM crash - disable tanstack extension integration (#10169)
  Create global keyboard shortcut handler (#10145)
  Warn when useState get/set names mismatch (#10166)
  Fix bottom bar badge text padding (#10162)
  Scale profile badge icons with font size (#10161)
  Add `no-extraneous-dependencies` and `no-nodejs-modules` eslint rules (#10151)
  Fix `useResolveUriQuery` (#10163)
  Nightly source-language update
  ...
This commit is contained in:
Eric Bailey
2026-04-07 14:25:55 -05:00
87 changed files with 921 additions and 1388 deletions
+60 -9
View File
@@ -431,16 +431,30 @@ yarn intl:compile # Compile translations for runtime
// src/state/queries/profile.ts
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
// Query key pattern
const RQKEY_ROOT = 'profile'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
import {createQueryKey} from '#/state/queries/util'
// Query hook
/*
* Query key name should match the query hook name for consistency
*/
const profileQueryKeyRoot = 'profile'
/*
* Use object params and createQueryKey helper for better readability and to
* avoid bugs with parameter order or types.
*/
export const createProfileQueryKey = (args: {did: string}) =>
createQueryKey(profileQueryKeyRoot, args)
/*
* Query hook should be named use[Name]Query, where [Name] describes the data
* being fetched. This is not a strict requirement, but it's a helpful
* convention for discoverability
*/
export function useProfileQuery({did}: {did: string}) {
const agent = useAgent()
return useQuery({
queryKey: RQKEY(did),
queryKey: createProfileQueryKey({did}),
queryFn: async () => {
const res = await agent.getProfile({actor: did})
return res.data
@@ -450,8 +464,12 @@ export function useProfileQuery({did}: {did: string}) {
})
}
// Mutation hook
export function useUpdateProfile() {
/*
* Mutation hook should match the name of the query hook, but with "Mutation"
* suffix. This is not a strict requirement, but it's a helpful convention for
* discoverability and consistency.
*/
export function useProfileMutation() {
const queryClient = useQueryClient()
return useMutation({
@@ -459,7 +477,9 @@ export function useUpdateProfile() {
// Update logic
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({queryKey: RQKEY(variables.did)})
queryClient.invalidateQueries({
queryKey: createProfileQueryKey({did: variables.did}),
})
},
onError: (error) => {
if (isNetworkError(error)) {
@@ -473,6 +493,24 @@ export function useUpdateProfile() {
}
})
}
/*
* If cache mutation is needed, include specific interfaces for the specific
* mutations you require adjacent to the source queries. Naming should be
* descriptive of the mutation's purpose, e.g. use[Name]CacheMutation. This is
* not a strict requirement, but it's a helpful convention for discoverability
* and consistency.
*/
export function useProfileCacheMutation() {
const queryClient = useQueryClient()
return (data: Partial<Profile>) => {
queryClient.setQueryData(createProfileQueryKey({did: data.did}), oldData => {
if (!oldData) return oldData
return {...oldData, ...data}
})
}
}
```
**Stale Time Constants** (from `src/state/queries/index.ts`):
@@ -491,7 +529,7 @@ export function useDraftsQuery() {
const agent = useAgent()
return useInfiniteQuery({
queryKey: ['drafts'],
queryKey: createQueryKey('drafts'),
queryFn: async ({pageParam}) => {
const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam})
return res.data
@@ -504,6 +542,19 @@ export function useDraftsQuery() {
To get all items from pages: `data?.pages.flatMap(page => page.items) ?? []`
**Persisted Queries**
To persist query data across app restarts, `createQueryKey` supports a third
parameter called `options`, which has a `persistedVersion` property. When this
property is set to a number, the query will be persisted.
When this property is updated (e.g. incremented), the persisted data will be cleared and replaced with the new data from the query function. This is useful for cases where the shape of the data has changed and old persisted data would no longer be valid.
```tsx
export const createProfileQueryKey = (args: {did: string}) =>
createQueryKey(profileQueryKeyRoot, args, {persistedVersion: 1})
```
### Preferences (React Context)
```tsx
+14 -1
View File
@@ -47,7 +47,6 @@ export default defineConfig(
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'],
@@ -62,6 +61,7 @@ export default defineConfig(
'react-native': reactNative,
'react-native-a11y': reactNativeA11y,
'simple-import-sort': simpleImportSort,
// @ts-expect-error - not sure why
lingui,
'react-compiler': reactCompiler,
'bsky-internal': bskyInternal,
@@ -127,6 +127,7 @@ export default defineConfig(
*/
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
'react/hook-use-state': 'warn',
'react/no-unescaped-entities': 'off',
'react/prop-types': 'off',
'react-native/no-inline-styles': 'off',
@@ -189,6 +190,18 @@ export default defineConfig(
*/
ignore: ['^#\/locale\/locales\/.+\/messages'],
}],
'import-x/no-extraneous-dependencies': ['error', {
'whitelist': [
// test files only
'@jest/globals',
// we only use a really simple util from this, and we know it will be present
'expo-modules-core',
// this is a dep for @atproto/api, but we absolutely need them in sync, so just
// rely on the transient version
'@atproto/common-web',
]
}],
'import-x/no-nodejs-modules': 'error',
/**
* TypeScript-specific rules
+1
View File
@@ -29,6 +29,7 @@ function getTagName(node) {
return reversedIdentifiers.reverse().join('.')
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
+1
View File
@@ -3,6 +3,7 @@ const BANNED_IMPORTS = [
'@fortawesome/free-solid-svg-icons',
]
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
+1
View File
@@ -10,6 +10,7 @@ const BANNED_IMPORT_PREFIXES = [
'view/',
]
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
+1
View File
@@ -9,6 +9,7 @@ jest.mock('@react-native-async-storage/async-storage', () =>
require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
)
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => {
// eslint-disable-next-line import-x/no-nodejs-modules
const {EventEmitter} = require('events')
return {
__esModule: true,
-23
View File
@@ -1,23 +0,0 @@
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {render} from '@testing-library/react-native'
import {ThemeProvider} from '../src/lib/ThemeContext'
import {type RootStoreModel, RootStoreProvider} from '../src/state'
const customRender = (ui: any, rootStore: RootStoreModel) =>
render(
<GestureHandlerRootView style={{flex: 1}}>
<RootStoreProvider value={rootStore}>
<ThemeProvider theme="light">
<SafeAreaProvider>{ui}</SafeAreaProvider>
</ThemeProvider>
</RootStoreProvider>
</GestureHandlerRootView>,
)
// re-export everything
export * from '@testing-library/react-native'
// override render method
export {customRender as render}
+9 -9
View File
@@ -81,7 +81,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.19.3",
"@atproto/api": "^0.19.5",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.7",
@@ -111,7 +111,6 @@
"@ipld/dag-cbor": "^9.2.0",
"@lingui/core": "^5.9.2",
"@lingui/react": "^5.9.2",
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
"@miblanchard/react-native-slider": "^2.6.0",
"@mozzius/expo-dynamic-app-icon": "^1.8.0",
"@react-native-async-storage/async-storage": "2.2.0",
@@ -119,9 +118,9 @@
"@react-navigation/native": "^7.1.33",
"@react-navigation/native-stack": "^7.14.4",
"@sentry/react-native": "~6.20.0",
"@tanstack/query-async-storage-persister": "^5.25.0",
"@tanstack/react-query": "5.25.0",
"@tanstack/react-query-persist-client": "^5.25.0",
"@tanstack/query-async-storage-persister": "^5.96.2",
"@tanstack/react-query": "^5.96.2",
"@tanstack/react-query-persist-client": "^5.96.2",
"@tiptap/core": "^2.9.1",
"@tiptap/extension-document": "^2.9.1",
"@tiptap/extension-hard-break": "^2.9.1",
@@ -169,6 +168,7 @@
"expo-location": "~19.0.8",
"expo-media-library": "~18.2.1",
"expo-notifications": "~0.32.16",
"expo-paste-input": "^0.1.12",
"expo-privacy-sensitive": "^0.1.0",
"expo-screen-orientation": "~9.0.8",
"expo-sharing": "~14.0.8",
@@ -202,6 +202,7 @@
"react": "19.1.0",
"react-compiler-runtime": "^19.1.0-rc.1",
"react-dom": "19.1.0",
"react-hotkeys-hook": "5.2.4",
"react-image-crop": "^11.0.7",
"react-is": "19",
"react-keyed-flatten-children": "^5.0.0",
@@ -249,7 +250,6 @@
"@lingui/cli": "^5.9.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@react-native/babel-preset": "0.81.5",
"@react-native/eslint-config": "^0.81.5",
"@react-native/typescript-config": "^0.81.5",
"@sentry/webpack-plugin": "^3.2.2",
"@testing-library/react-native": "^13.2.0",
@@ -267,8 +267,8 @@
"eslint": "^9.39.2",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-bsky-internal": "link:./eslint",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-lingui": "^0.11.0",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-lingui": "^0.12.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react-hooks": "^7.0.1",
@@ -290,7 +290,7 @@
"svgo": "^3.3.2",
"ts-plugin-sort-import-suggestions": "^1.0.4",
"typescript": "^6.0.2",
"typescript-eslint": "^8.57.2",
"typescript-eslint": "^8.58.0",
"webpack-bundle-analyzer": "^4.10.1"
},
"resolutions": {
@@ -1,13 +0,0 @@
diff --git a/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt b/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt
index 4ed2307..ede1181 100644
--- a/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt
+++ b/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt
@@ -54,7 +54,7 @@ class PasteTextInputManager(context: ReactApplicationContext) : ReactTextInputMa
}
override fun getExportedCustomBubblingEventTypeConstants(): MutableMap<String, Any> {
- val map = super.getExportedCustomBubblingEventTypeConstants()!!
+ val map = super.getExportedCustomBubblingEventTypeConstants().toMutableMap()
map["onPaste"] = MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onPaste")
@@ -1,264 +0,0 @@
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
index e916023..5049c33 100644
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
@@ -4,6 +4,7 @@
//
// Created by Elias Nahum on 04-11-20.
// Copyright © 2020 Facebook. All rights reserved.
+// Updated to remove parents default text view
//
#import "PasteInputView.h"
@@ -12,49 +13,78 @@
@implementation PasteInputView
{
- PasteInputTextView *_backedTextInputView;
+ // We'll store the custom text view in this ivar
+ PasteInputTextView *_customBackedTextView;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
+ // Must call the supers designated initializer
if (self = [super initWithBridge:bridge]) {
- _backedTextInputView = [[PasteInputTextView alloc] initWithFrame:self.bounds];
- _backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- _backedTextInputView.textInputDelegate = self;
+ // 1. The parent (RCTMultilineTextInputView) has already created
+ // its own _backedTextInputView = [RCTUITextView new] in super init.
+ // We can remove that subview:
- [self addSubview:_backedTextInputView];
- }
+ id<RCTBackedTextInputViewProtocol> parentInputView = super.backedTextInputView;
+ if ([parentInputView isKindOfClass:[UIView class]]) {
+ UIView *parentSubview = (UIView *)parentInputView;
+ if (parentSubview.superview == self) {
+ [parentSubview removeFromSuperview];
+ }
+ }
+ // 2. Now create our custom PasteInputTextView
+ _customBackedTextView = [[PasteInputTextView alloc] initWithFrame:self.bounds];
+ _customBackedTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ _customBackedTextView.textInputDelegate = self;
+
+ // Optional: disable inline predictions for iOS 17+
+ if (@available(iOS 17.0, *)) {
+ _customBackedTextView.inlinePredictionType = UITextInlinePredictionTypeNo;
+ }
+
+ // 3. Add your custom text view as the only subview
+ [self addSubview:_customBackedTextView];
+ }
return self;
}
+/**
+ * Override the parent's accessor so that anywhere in RN that calls
+ * `self.backedTextInputView` will get the custom PasteInputTextView.
+ */
- (id<RCTBackedTextInputViewProtocol>)backedTextInputView
{
- return _backedTextInputView;
+ return _customBackedTextView;
}
-- (void)setDisableCopyPaste:(BOOL)disableCopyPaste {
- _backedTextInputView.disableCopyPaste = disableCopyPaste;
+#pragma mark - Setters for React Props
+
+- (void)setDisableCopyPaste:(BOOL)disableCopyPaste
+{
+ _customBackedTextView.disableCopyPaste = disableCopyPaste;
}
-- (void)setOnPaste:(RCTDirectEventBlock)onPaste {
- _backedTextInputView.onPaste = onPaste;
+- (void)setOnPaste:(RCTDirectEventBlock)onPaste
+{
+ _customBackedTextView.onPaste = onPaste;
}
-- (void)setSmartPunctuation:(NSString *)smartPunctuation {
- if ([smartPunctuation isEqualToString:@"enable"]) {
- [_backedTextInputView setSmartDashesType:UITextSmartDashesTypeYes];
- [_backedTextInputView setSmartQuotesType:UITextSmartQuotesTypeYes];
- [_backedTextInputView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeYes];
- } else if ([smartPunctuation isEqualToString:@"disable"]) {
- [_backedTextInputView setSmartDashesType:UITextSmartDashesTypeNo];
- [_backedTextInputView setSmartQuotesType:UITextSmartQuotesTypeNo];
- [_backedTextInputView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeNo];
- } else {
- [_backedTextInputView setSmartDashesType:UITextSmartDashesTypeDefault];
- [_backedTextInputView setSmartQuotesType:UITextSmartQuotesTypeDefault];
- [_backedTextInputView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeDefault];
- }
+- (void)setSmartPunctuation:(NSString *)smartPunctuation
+{
+ if ([smartPunctuation isEqualToString:@"enable"]) {
+ [_customBackedTextView setSmartDashesType:UITextSmartDashesTypeYes];
+ [_customBackedTextView setSmartQuotesType:UITextSmartQuotesTypeYes];
+ [_customBackedTextView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeYes];
+ } else if ([smartPunctuation isEqualToString:@"disable"]) {
+ [_customBackedTextView setSmartDashesType:UITextSmartDashesTypeNo];
+ [_customBackedTextView setSmartQuotesType:UITextSmartQuotesTypeNo];
+ [_customBackedTextView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeNo];
+ } else {
+ [_customBackedTextView setSmartDashesType:UITextSmartDashesTypeDefault];
+ [_customBackedTextView setSmartQuotesType:UITextSmartQuotesTypeDefault];
+ [_customBackedTextView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeDefault];
+ }
}
#pragma mark - UIScrollViewDelegate
@@ -62,7 +92,6 @@ - (void)setSmartPunctuation:(NSString *)smartPunctuation {
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
RCTDirectEventBlock onScroll = self.onScroll;
-
if (onScroll) {
CGPoint contentOffset = scrollView.contentOffset;
CGSize contentSize = scrollView.contentSize;
@@ -71,22 +100,22 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
onScroll(@{
@"contentOffset": @{
- @"x": @(contentOffset.x),
- @"y": @(contentOffset.y)
+ @"x": @(contentOffset.x),
+ @"y": @(contentOffset.y)
},
@"contentInset": @{
- @"top": @(contentInset.top),
- @"left": @(contentInset.left),
- @"bottom": @(contentInset.bottom),
- @"right": @(contentInset.right)
+ @"top": @(contentInset.top),
+ @"left": @(contentInset.left),
+ @"bottom": @(contentInset.bottom),
+ @"right": @(contentInset.right)
},
@"contentSize": @{
- @"width": @(contentSize.width),
- @"height": @(contentSize.height)
+ @"width": @(contentSize.width),
+ @"height": @(contentSize.height)
},
@"layoutMeasurement": @{
- @"width": @(size.width),
- @"height": @(size.height)
+ @"width": @(size.width),
+ @"height": @(size.height)
},
@"zoomScale": @(scrollView.zoomScale ?: 1),
});
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
index dd50053..2ed7017 100644
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
@@ -122,8 +122,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
const auto &newTextInputProps = static_cast<const PasteTextInputProps &>(*props);
// Traits:
- if (newTextInputProps.traits.multiline != oldTextInputProps.traits.multiline) {
- [self _setMultiline:newTextInputProps.traits.multiline];
+ if (newTextInputProps.multiline != oldTextInputProps.multiline) {
+ [self _setMultiline:newTextInputProps.multiline];
}
if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) {
@@ -421,7 +421,7 @@ - (void)textInputDidChangeSelection
return;
}
const auto &props = static_cast<const PasteTextInputProps &>(*_props);
- if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
+ if (props.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
[self textInputDidChange];
_ignoreNextTextInputCall = YES;
}
@@ -708,11 +708,11 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe
- (SubmitBehavior)getSubmitBehavior
{
const auto &props = static_cast<const PasteTextInputProps &>(*_props);
- const SubmitBehavior submitBehaviorDefaultable = props.traits.submitBehavior;
+ const SubmitBehavior submitBehaviorDefaultable = props.submitBehavior;
// We should always have a non-default `submitBehavior`, but in case we don't, set it based on multiline.
if (submitBehaviorDefaultable == SubmitBehavior::Default) {
- return props.traits.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
+ return props.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
}
return submitBehaviorDefaultable;
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
index 29e094f..7ef519a 100644
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
@@ -22,8 +22,7 @@ PasteTextInputProps::PasteTextInputProps(
const PropsParserContext &context,
const PasteTextInputProps &sourceProps,
const RawProps& rawProps)
- : ViewProps(context, sourceProps, rawProps),
- BaseTextProps(context, sourceProps, rawProps),
+ : BaseTextInputProps(context, sourceProps, rawProps),
traits(convertRawProp(context, rawProps, sourceProps.traits, {})),
smartPunctuation(convertRawProp(context, rawProps, "smartPunctuation", sourceProps.smartPunctuation, {})),
disableCopyPaste(convertRawProp(context, rawProps, "disableCopyPaste", sourceProps.disableCopyPaste, {false})),
@@ -133,7 +132,7 @@ TextAttributes PasteTextInputProps::getEffectiveTextAttributes(Float fontSizeMul
ParagraphAttributes PasteTextInputProps::getEffectiveParagraphAttributes() const {
auto result = paragraphAttributes;
- if (!traits.multiline) {
+ if (!multiline) {
result.maximumNumberOfLines = 1;
}
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
index 723d00c..31cfe66 100644
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
@@ -15,6 +15,7 @@
#include <react/renderer/components/iostextinput/conversions.h>
#include <react/renderer/components/iostextinput/primitives.h>
#include <react/renderer/components/text/BaseTextProps.h>
+#include <react/renderer/components/textinput/BaseTextInputProps.h>
#include <react/renderer/components/view/ViewProps.h>
#include <react/renderer/core/Props.h>
#include <react/renderer/core/PropsParserContext.h>
@@ -25,7 +26,7 @@
namespace facebook::react {
-class PasteTextInputProps final : public ViewProps, public BaseTextProps {
+class PasteTextInputProps final : public BaseTextInputProps {
public:
PasteTextInputProps() = default;
PasteTextInputProps(const PropsParserContext& context, const PasteTextInputProps& sourceProps, const RawProps& rawProps);
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
index 31e07e3..7f0ebfb 100644
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
@@ -91,20 +91,11 @@ void PasteTextInputShadowNode::updateStateIfNeeded(
const auto& state = getStateData();
react_native_assert(textLayoutManager_);
- react_native_assert(
- (!state.layoutManager || state.layoutManager == textLayoutManager_) &&
- "`StateData` refers to a different `TextLayoutManager`");
-
- if (state.reactTreeAttributedString == reactTreeAttributedString &&
- state.layoutManager == textLayoutManager_) {
- return;
- }
auto newState = TextInputState{};
newState.attributedStringBox = AttributedStringBox{reactTreeAttributedString};
newState.paragraphAttributes = getConcreteProps().paragraphAttributes;
newState.reactTreeAttributedString = reactTreeAttributedString;
- newState.layoutManager = textLayoutManager_;
newState.mostRecentEventCount = getConcreteProps().mostRecentEventCount;
setStateData(std::move(newState));
}
+15 -17
View File
@@ -11,13 +11,11 @@ import {
import * as ScreenOrientation from 'expo-screen-orientation'
import * as SplashScreen from 'expo-splash-screen'
import * as SystemUI from 'expo-system-ui'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import * as Sentry from '@sentry/react-native'
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
import {QueryProvider} from '#/lib/react-query'
import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext'
import {Provider as TranslateOnDeviceProvider} from '#/lib/translation'
import I18nProvider from '#/locale/i18nProvider'
@@ -59,7 +57,7 @@ import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf'
import {atoms as a, ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
@@ -89,9 +87,9 @@ import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
SplashScreen.preventAutoHideAsync()
void SplashScreen.preventAutoHideAsync()
if (IS_IOS) {
SystemUI.setBackgroundColorAsync('black')
void SystemUI.setBackgroundColorAsync('black')
}
if (IS_ANDROID) {
// iOS is handled by the config plugin -sfn
@@ -105,17 +103,17 @@ if (IS_ANDROID) {
/**
* Begin geolocation ASAP
*/
Geo.resolve()
prefetchAgeAssuranceConfig()
prefetchLiveEvents()
prefetchAppConfig()
void Geo.resolve()
void prefetchAgeAssuranceConfig()
void prefetchLiveEvents()
void prefetchAppConfig()
function InnerApp() {
const [isReady, setIsReady] = useState(false)
const {currentAccount} = useSession()
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const hasCheckedReferrer = useStarterPackEntry()
// init
@@ -134,16 +132,16 @@ function InnerApp() {
}
}
const account = readLastActiveAccount()
onLaunch(account)
void onLaunch(account)
}, [resumeSession])
useEffect(() => {
return listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please sign in again.`), {
Toast.show(l`Sorry! Your session expired. Please sign in again.`, {
type: 'info',
})
})
}, [_])
}, [l])
return (
<Alf theme={theme}>
@@ -176,7 +174,7 @@ function InnerApp() {
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
style={a.h_full}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TranslateOnDeviceProvider>
@@ -220,8 +218,8 @@ function App() {
const [isReady, setReady] = useState(false)
useEffect(() => {
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() =>
setReady(true),
void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(
() => setReady(true),
)
}, [])
+17 -15
View File
@@ -5,10 +5,10 @@ import './style.css'
import {Fragment, useEffect, useState} from 'react'
import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import * as Sentry from '@sentry/react-native'
import {Provider as HotkeysProvider} from '#/lib/hotkeys'
import {QueryProvider} from '#/lib/react-query'
import {ThemeProvider} from '#/lib/ThemeContext'
import {Provider as TranslateOnDeviceProvider} from '#/lib/translation'
@@ -82,17 +82,17 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom
/**
* Begin geolocation ASAP
*/
Geo.resolve()
prefetchAgeAssuranceConfig()
prefetchLiveEvents()
prefetchAppConfig()
void Geo.resolve()
void prefetchAgeAssuranceConfig()
void prefetchLiveEvents()
void prefetchAppConfig()
function InnerApp() {
const [isReady, setIsReady] = useState(false)
const {currentAccount} = useSession()
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const hasCheckedReferrer = useStarterPackEntry()
// init
@@ -105,22 +105,22 @@ function InnerApp() {
await features.init
}
} catch (e) {
logger.error(`session: resumeSession failed`, {message: e})
logger.error('session: resumeSession failed', {message: e})
} finally {
setIsReady(true)
}
}
const account = readLastActiveAccount()
onLaunch(account)
void onLaunch(account)
}, [resumeSession])
useEffect(() => {
return listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please sign in again.`), {
Toast.show(l`Sorry! Your session expired. Please sign in again.`, {
type: 'info',
})
})
}, [_])
}, [l])
return (
<Alf theme={theme}>
@@ -156,8 +156,10 @@ function InnerApp() {
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<TranslateOnDeviceProvider>
<Shell />
<ToastOutlet />
<HotkeysProvider>
<Shell />
<ToastOutlet />
</HotkeysProvider>
</TranslateOnDeviceProvider>
</IntentDialogProvider>
</HideBottomBarBorderProvider>
@@ -195,8 +197,8 @@ function App() {
const [isReady, setReady] = useState(false)
useEffect(() => {
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() =>
setReady(true),
void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(
() => setReady(true),
)
}, [])
+8 -5
View File
@@ -1,12 +1,14 @@
import {Children} from 'react'
import {type TextProps as RNTextProps} from 'react-native'
import {type StyleProp, type TextStyle} from 'react-native'
import {
type StyleProp,
type TextProps as RNTextProps,
type TextStyle,
} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import createEmojiRegex from 'emoji-regex'
import {type Alf, applyFonts, atoms, flatten} from '#/alf'
import {IS_NATIVE} from '#/env'
import {IS_IOS} from '#/env'
import {IS_IOS, IS_NATIVE} from '#/env'
/**
* Ensures that `lineHeight` defaults to a relative value of `1`, or applies
@@ -107,7 +109,8 @@ export function renderChildrenWithEmoji(
})
}
const SINGLE_EMOJI_RE = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u
const SINGLE_EMOJI_RE =
/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\uFE0F\u200D]+$/u
export function isOnlyEmoji(text: string) {
return text.length <= 15 && SINGLE_EMOJI_RE.test(text)
}
+1
View File
@@ -110,6 +110,7 @@ const Context = createContext<AnalyticsBaseContextType>({
},
},
})
Context.displayName = 'AnalyticsContext'
/**
* Ensures that deviceId is set and migrated from legacy storage. Handled on
+32 -36
View File
@@ -560,32 +560,30 @@ export function ProfileGrid({
<Text style={[a.text_sm, a.font_semi_bold, t.atoms.text]}>
<Trans>Suggested for you</Trans>
</Text>
{!isProfileHeaderContext && (
<Button
label={l`See more suggested profiles`}
onPress={() => {
followDialogControl.open()
ax.metric('suggestedUser:seeMore', {
logContext,
recId,
})
}}>
{({hovered}) => (
<Text
style={[
a.text_sm,
{color: t.palette.primary_500},
hovered &&
web({
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
}),
]}>
<Trans>See more</Trans>
</Text>
)}
</Button>
)}
<Button
label={l`See more suggested profiles`}
onPress={() => {
followDialogControl.open()
ax.metric('suggestedUser:seeMore', {
logContext,
recId,
})
}}>
{({hovered}) => (
<Text
style={[
a.text_sm,
{color: t.palette.primary_500},
hovered &&
web({
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
}),
]}>
<Trans>See more</Trans>
</Text>
)}
</Button>
</View>
<FollowDialogWithoutGuide control={followDialogControl} />
<LayoutAnimationConfig skipExiting skipEntering>
@@ -605,16 +603,14 @@ export function ProfileGrid({
decelerationRate="fast">
{content}
{!isProfileHeaderContext && (
<SeeMoreSuggestedProfilesCard
onPress={() => {
followDialogControl.open()
ax.metric('suggestedUser:seeMore', {
logContext,
})
}}
/>
)}
<SeeMoreSuggestedProfilesCard
onPress={() => {
followDialogControl.open()
ax.metric('suggestedUser:seeMore', {
logContext,
})
}}
/>
</ScrollView>
</BlockDrawerGesture>
)}
@@ -13,6 +13,7 @@ import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
import {KeepAwake} from '#/components/KeepAwake'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {GifPresentationControls} from '../GifPresentationControls'
@@ -112,6 +113,7 @@ export function VideoEmbedInnerNative({
/>
)}
<MediaInsetBorder />
<KeepAwake enabled={isPlaying} />
</View>
)
}
@@ -136,6 +136,8 @@ export const BookmarkButton = memo(function BookmarkButton({
<PostControlButton
testID="postBookmarkBtn"
big={big}
active={isBookmarked}
activeColor={t.palette.primary_500}
label={
isBookmarked
? _(msg`Remove from saved posts`)
@@ -143,10 +145,7 @@ export const BookmarkButton = memo(function BookmarkButton({
}
onPress={onHandlePress}
hitSlop={hitSlop}>
<PostControlButtonIcon
fill={isBookmarked ? t.palette.primary_500 : undefined}
icon={isBookmarked ? BookmarkFilled : Bookmark}
/>
<PostControlButtonIcon icon={isBookmarked ? BookmarkFilled : Bookmark} />
</PostControlButton>
)
})
@@ -130,8 +130,11 @@ export function PostControlButtonText({style, ...props}: TextProps) {
<Text
style={[
color,
a.user_select_none,
big ? a.text_md : a.text_sm,
active && a.font_semi_bold,
// prevent layout shift on android
{includeFontPadding: false, textAlignVertical: 'center'},
style,
]}
{...props}
+11 -4
View File
@@ -24,7 +24,7 @@ import {
ProgressGuideAction,
useProgressGuideControls,
} from '#/state/shell/progress-guide'
import {atoms as a, useBreakpoints} from '#/alf'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Reply as Bubble} from '#/components/icons/Reply'
import {useFormatPostStatCount} from '#/components/PostControls/util'
import * as Skele from '#/components/Skeleton'
@@ -74,6 +74,7 @@ let PostControls = ({
forceGoogleTranslate?: boolean
}): React.ReactNode => {
const ax = useAnalytics()
const t = useTheme()
const {t: l} = useLingui()
const {openComposer} = useOpenComposer()
const {feedDescriptor} = useFeedFeedbackContext()
@@ -270,6 +271,8 @@ let PostControls = ({
<PostControlButton
testID="likeBtn"
big={big}
active={Boolean(post.viewer?.like)}
activeColor={t.palette.pink}
onPress={() => requireAuth(() => onPressToggleLike())}
label={
post.viewer?.like
@@ -296,10 +299,14 @@ let PostControls = ({
hasBeenToggled={hasLikeIconBeenToggled}
/>
<CountWheel
likeCount={post.likeCount ?? 0}
big={big}
isLiked={Boolean(post.viewer?.like)}
count={post.likeCount ?? 0}
isToggled={Boolean(post.viewer?.like)}
hasBeenToggled={hasLikeIconBeenToggled}
renderCount={({count}) => (
<PostControlButtonText>
{formatPostStatCount(count)}
</PostControlButtonText>
)}
/>
</PostControlButton>
</View>
+15 -6
View File
@@ -1,7 +1,7 @@
import {View} from 'react-native'
import {useWindowDimensions, View} from 'react-native'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {atoms as a, type ViewStyleProp} from '#/alf'
import {atoms as a, useAlf, type ViewStyleProp} from '#/alf'
import {BotBadge, BotBadgeButton, isBotAccount} from '#/components/BotBadge'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
@@ -38,12 +38,21 @@ export function ProfileBadges({
}) {
const shadowed = useProfileShadow(profile)
const verification = useSimpleVerificationState({profile})
const {fontScale: nativeScaleMultiplier} = useWindowDimensions()
const {
fonts: {scaleMultiplier: alfScaleMultiplier},
} = useAlf()
// if nothing to show, don't render the container at all
if (!verification.showBadge && !isBotAccount(shadowed)) return null
const isOnTheSmallSide = size === 'xs' || size === 'sm'
const verificationIconWidth =
verificationIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier
const botIconWidth =
botIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier
return (
<View
style={[
@@ -56,19 +65,19 @@ export function ProfileBadges({
<>
<VerificationCheckButton
profile={shadowed}
width={verificationIconSizes[size]}
width={verificationIconWidth}
/>
<BotBadgeButton profile={shadowed} width={botIconSizes[size]} />
<BotBadgeButton profile={shadowed} width={botIconWidth} />
</>
) : (
<>
{verification.showBadge && (
<VerificationCheck
verifier={verification.role === 'verifier'}
width={verificationIconSizes[size]}
width={verificationIconWidth}
/>
)}
<BotBadge profile={shadowed} width={botIconSizes[size]} />
<BotBadge profile={shadowed} width={botIconWidth} />
</>
)}
</View>
+2 -3
View File
@@ -3,8 +3,7 @@ import {View} from 'react-native'
import {Select as RadixSelect} from 'radix-ui'
import {useA11y} from '#/state/a11y'
import {flatten, useTheme, web} from '#/alf'
import {atoms as a} from '#/alf'
import {atoms as a, flatten, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {
@@ -109,7 +108,7 @@ export function Trigger({children, label}: TriggerProps) {
borderRadius: 10,
maxWidth: 400,
outline: 0,
borderWidth: 2,
borderWidth: 1,
borderStyle: 'solid',
borderColor: focused
? t.palette.primary_500
@@ -46,6 +46,7 @@ export const PostsList = forwardRef<SectionRef, ProfilesListProps>(
return (
<View>
<PostFeed
enabled
feed={feed}
pollInterval={60e3}
scrollElRef={scrollElRef}
+5 -1
View File
@@ -1,6 +1,6 @@
import {View} from 'react-native'
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
import {atoms as a, useTheme, type ViewStyleProp, web as webOnly} from '#/alf'
import {IS_NATIVE, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
export function SubtleHover({
@@ -33,6 +33,10 @@ export function SubtleHover({
a.transition_opacity,
t.atoms.bg_contrast_50,
style,
// Force Safari to composite the overlay on its own GPU layer.
// This fixes a layout shift that happens due to different subpixel
// rounding when the overlay is composited on hover.
webOnly({willChange: 'opacity'}),
{opacity: hover ? opacity : 0},
]}
/>
@@ -1,6 +1,6 @@
import {useState} from 'react'
import {View} from 'react-native'
import {XRPCError} from '@atproto/xrpc'
import {XRPCError} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
+1 -1
View File
@@ -1,5 +1,5 @@
import {useEffect} from 'react'
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
const events = new EventEmitter<{
emailVerified: void
@@ -63,7 +63,7 @@ export function DateFieldButton({
paddingLeft: 14,
paddingRight: 14,
borderColor: 'transparent',
borderWidth: 2,
borderWidth: 1,
},
native({
paddingTop: 10,
+79 -62
View File
@@ -1,8 +1,9 @@
import {forwardRef} from 'react'
import {useEffect, useRef} from 'react'
import {type TextInput, View} from 'react-native'
import {useLingui} from '@lingui/react/macro'
import {HITSLOP_10} from '#/lib/constants'
import {listenFocusSearch} from '#/state/events'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
@@ -10,73 +11,89 @@ import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {IS_NATIVE} from '#/env'
type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
type Props = Omit<TextField.InputProps, 'label'> & {
label?: TextField.InputProps['label']
/**
* Called when the user presses the (X) button
*/
onClearText?: () => void
hotkey?: boolean
ref?: React.RefObject<TextInput | null>
}
export const SearchInput = forwardRef<TextInput, SearchInputProps>(
function SearchInput({value, label, onClearText, ...rest}, ref) {
const t = useTheme()
const {t: l} = useLingui()
const showClear = value && value.length > 0
export function SearchInput({
value,
label,
onClearText,
hotkey,
ref,
...rest
}: Props) {
const t = useTheme()
const {t: l} = useLingui()
const showClear = value && value.length > 0
const internalRef = useRef<TextInput>(null)
const inputRef = ref ?? internalRef
return (
<View style={[a.w_full, a.relative]}>
<TextField.Root>
<TextField.Icon icon={MagnifyingGlassIcon} />
<TextField.Input
inputRef={ref}
label={label || l`Search`}
value={value}
placeholder={l`Search`}
returnKeyType="search"
keyboardAppearance={t.scheme}
selectTextOnFocus={IS_NATIVE}
autoFocus={false}
accessibilityRole="search"
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
style={[
showClear
? {
paddingRight: 24,
}
: {},
]}
{...rest}
/>
</TextField.Root>
useEffect(() => {
if (!hotkey) return
return listenFocusSearch(() => {
inputRef.current?.focus()
})
}, [hotkey, inputRef])
{showClear && (
<View
style={[
a.absolute,
a.z_20,
a.my_auto,
a.inset_0,
a.justify_center,
a.pr_sm,
{left: 'auto'},
]}>
<Button
testID="searchTextInputClearBtn"
onPress={onClearText}
label={l`Clear search query`}
hitSlop={HITSLOP_10}
size="tiny"
shape="round"
variant="ghost"
color="secondary">
<ButtonIcon icon={X} size="xs" />
</Button>
</View>
)}
</View>
)
},
)
return (
<View style={[a.w_full, a.relative]}>
<TextField.Root>
<TextField.Icon icon={MagnifyingGlassIcon} />
<TextField.Input
inputRef={inputRef}
label={label || l`Search`}
value={value}
placeholder={l`Search`}
returnKeyType="search"
keyboardAppearance={t.scheme}
selectTextOnFocus={IS_NATIVE}
autoFocus={false}
accessibilityRole="search"
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
style={[
showClear
? {
paddingRight: 24,
}
: {},
]}
{...rest}
/>
</TextField.Root>
{showClear && (
<View
style={[
a.absolute,
a.z_20,
a.my_auto,
a.inset_0,
a.justify_center,
a.pr_sm,
{left: 'auto'},
]}>
<Button
testID="searchTextInputClearBtn"
onPress={onClearText}
label={l`Clear search query`}
hitSlop={HITSLOP_10}
size="tiny"
shape="round"
variant="ghost"
color="secondary">
<ButtonIcon icon={X} size="xs" />
</Button>
</View>
)}
</View>
)
}
@@ -1,7 +1,7 @@
import {useCallback, useMemo, useState} from 'react'
import {View} from 'react-native'
import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api'
import {XRPCError} from '@atproto/xrpc'
import {XRPCError} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -184,7 +184,7 @@ function Inner(props: ReportDialogProps) {
)
})
}, [
props,
props.subject,
allLabelers,
state.selectedOption,
isBskyOnlyReason,
@@ -241,7 +241,17 @@ function Inner(props: ReportDialogProps) {
} finally {
setPending(false)
}
}, [_, submitReport, state, dispatch, props, setPending, setSuccess])
}, [
_,
submitReport,
state,
dispatch,
props.subject,
props.control,
props.onAfterSubmit,
setPending,
setSuccess,
])
useCallOnce(() => {
ax.metric('reportDialog:open', {
+8
View File
@@ -10,6 +10,10 @@ const iOSMajorVersion =
Platform.OS === 'ios' && typeof Platform.Version === 'string'
? parseInt(Platform.Version.split('.')[0], 10)
: 0
const androidPlatformVersion =
Platform.OS === 'android' && typeof Platform.Version === 'number'
? Platform.Version
: 0
/**
* The semver version of the app, specified in our `package.json`.file. On
@@ -49,3 +53,7 @@ export const IS_WEB_FIREFOX: boolean = false
export const IS_HIGH_DPI: boolean = true
// ideally we'd use isLiquidGlassAvailable() from expo-glass-effect but checking iOS version is good enough for now
export const IS_LIQUID_GLASS: boolean = iOSMajorVersion >= 26
// So we can avoid attempting on-device translation when we know it's unsupported.
export const IS_TRANSLATION_SUPPORTED: boolean =
(IS_IOS && iOSMajorVersion >= 18) ||
(IS_ANDROID && androidPlatformVersion > 22)
+1
View File
@@ -48,3 +48,4 @@ export const IS_HIGH_DPI: boolean = window.matchMedia(
'(min-resolution: 2dppx)',
).matches
export const IS_LIQUID_GLASS: boolean = false
export const IS_TRANSLATION_SUPPORTED: boolean = false
+1 -1
View File
@@ -1,5 +1,5 @@
import {useEffect, useState} from 'react'
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
import {networkRetry} from '#/lib/async/retry'
import {
+20 -46
View File
@@ -8,10 +8,7 @@ import Animated, {
} from 'react-native-reanimated'
import {decideShouldRoll} from '#/lib/custom-animations/util'
import {s} from '#/lib/styles'
import {Text} from '#/view/com/util/text/Text'
import {atoms as a, useTheme} from '#/alf'
import {useFormatPostStatCount} from '#/components/PostControls/util'
import {atoms as a} from '#/alf'
const animationConfig = {
duration: 400,
@@ -87,89 +84,66 @@ function ExitingDown() {
}
export function CountWheel({
likeCount,
big,
isLiked,
count,
isToggled,
hasBeenToggled,
renderCount,
}: {
likeCount: number
big?: boolean
isLiked: boolean
count: number
isToggled: boolean
hasBeenToggled: boolean
renderCount: (props: {count: number}) => React.ReactNode
}) {
const t = useTheme()
const shouldAnimate = !useReducedMotion() && hasBeenToggled
const shouldRoll = decideShouldRoll(isLiked, likeCount)
const shouldRoll = decideShouldRoll(isToggled, count)
// Incrementing the key will cause the `Animated.View` to re-render, with the newly selected entering/exiting
// animation
// The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would
// be unnecessary
const [key, setKey] = useState(0)
const [prevCount, setPrevCount] = useState(likeCount)
const prevIsLiked = useRef(isLiked)
const formatPostStatCount = useFormatPostStatCount()
const formattedCount = formatPostStatCount(likeCount)
const formattedPrevCount = formatPostStatCount(prevCount)
const [prevCount, setPrevCount] = useState(count)
const prevIsToggled = useRef(isToggled)
useEffect(() => {
if (isLiked === prevIsLiked.current) {
if (isToggled === prevIsToggled.current) {
return
}
const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1
const newPrevCount = isToggled ? count - 1 : count + 1
setKey(prev => prev + 1)
setPrevCount(newPrevCount)
prevIsLiked.current = isLiked
}, [isLiked, likeCount])
prevIsToggled.current = isToggled
}, [isToggled, count])
const enteringAnimation =
shouldAnimate && shouldRoll
? isLiked
? isToggled
? EnteringUp
: EnteringDown
: undefined
const exitingAnimation =
shouldAnimate && shouldRoll
? isLiked
? isToggled
? ExitingUp
: ExitingDown
: undefined
return (
<LayoutAnimationConfig skipEntering skipExiting>
{likeCount > 0 ? (
{count > 0 ? (
<View style={[a.justify_center]}>
<Animated.View entering={enteringAnimation} key={key}>
<Text
testID="likeCount"
style={[
big ? a.text_md : a.text_sm,
a.user_select_none,
isLiked
? [a.font_semi_bold, s.likeColor]
: {color: t.palette.contrast_500},
]}>
{formattedCount}
</Text>
{renderCount({count})}
</Animated.View>
{shouldAnimate && (likeCount > 1 || !isLiked) ? (
{shouldAnimate && (count > 1 || !isToggled) ? (
<Animated.View
entering={exitingAnimation}
// Add 2 to the key so there are never duplicates
key={key + 2}
style={[a.absolute, {width: 50, opacity: 0}]}
aria-disabled={true}>
<Text
style={[
big ? a.text_md : a.text_sm,
a.user_select_none,
isLiked
? [a.font_semi_bold, s.likeColor]
: {color: t.palette.contrast_500},
]}>
{formattedPrevCount}
</Text>
{renderCount({count: prevCount})}
</Animated.View>
) : null}
</View>
+19 -46
View File
@@ -3,10 +3,6 @@ import {View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated'
import {decideShouldRoll} from '#/lib/custom-animations/util'
import {s} from '#/lib/styles'
import {Text} from '#/view/com/util/text/Text'
import {atoms as a, useTheme} from '#/alf'
import {useFormatPostStatCount} from '#/components/PostControls/util'
const animationConfig = {
duration: 400,
@@ -35,50 +31,46 @@ const exitingDownKeyframe = [
]
export function CountWheel({
likeCount,
big,
isLiked,
count,
isToggled,
hasBeenToggled,
renderCount,
}: {
likeCount: number
big?: boolean
isLiked: boolean
count: number
isToggled: boolean
hasBeenToggled: boolean
renderCount: (props: {count: number}) => React.ReactNode
}) {
const t = useTheme()
const shouldAnimate = !useReducedMotion() && hasBeenToggled
const shouldRoll = decideShouldRoll(isLiked, likeCount)
const shouldRoll = decideShouldRoll(isToggled, count)
const countView = useRef<HTMLDivElement>(null)
const prevCountView = useRef<HTMLDivElement>(null)
const [prevCount, setPrevCount] = useState(likeCount)
const prevIsLiked = useRef(isLiked)
const formatPostStatCount = useFormatPostStatCount()
const formattedCount = formatPostStatCount(likeCount)
const formattedPrevCount = formatPostStatCount(prevCount)
const [prevCount, setPrevCount] = useState(count)
const prevIsToggled = useRef(isToggled)
useEffect(() => {
if (isLiked === prevIsLiked.current) {
if (isToggled === prevIsToggled.current) {
return
}
const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1
const newPrevCount = isToggled ? count - 1 : count + 1
if (shouldAnimate && shouldRoll) {
countView.current?.animate?.(
isLiked ? enteringUpKeyframe : enteringDownKeyframe,
isToggled ? enteringUpKeyframe : enteringDownKeyframe,
animationConfig,
)
prevCountView.current?.animate?.(
isLiked ? exitingUpKeyframe : exitingDownKeyframe,
isToggled ? exitingUpKeyframe : exitingDownKeyframe,
animationConfig,
)
setPrevCount(newPrevCount)
}
prevIsLiked.current = isLiked
}, [isLiked, likeCount, shouldAnimate, shouldRoll])
prevIsToggled.current = isToggled
}, [isToggled, count, shouldAnimate, shouldRoll])
if (likeCount < 1) {
if (count < 1) {
return null
}
@@ -87,34 +79,15 @@ export function CountWheel({
<View
// @ts-expect-error is div
ref={countView}>
<Text
testID="likeCount"
style={[
big ? a.text_md : a.text_sm,
a.user_select_none,
isLiked
? [a.font_semi_bold, s.likeColor]
: {color: t.palette.contrast_500},
]}>
{formattedCount}
</Text>
{renderCount({count})}
</View>
{shouldAnimate && (likeCount > 1 || !isLiked) ? (
{shouldAnimate && (count > 1 || !isToggled) ? (
<View
style={{position: 'absolute', opacity: 0}}
aria-disabled={true}
// @ts-expect-error is div
ref={prevCountView}>
<Text
style={[
big ? a.text_md : a.text_sm,
a.user_select_none,
isLiked
? [a.font_semi_bold, s.likeColor]
: {color: t.palette.contrast_500},
]}>
{formattedPrevCount}
</Text>
{renderCount({count: prevCount})}
</View>
) : null}
</View>
+2 -3
View File
@@ -5,7 +5,6 @@ import Animated, {
useReducedMotion,
} from 'react-native-reanimated'
import {s} from '#/lib/styles'
import {useTheme} from '#/alf'
import {
Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled,
@@ -86,7 +85,7 @@ export function AnimatedLikeIcon({
{isLiked ? (
<Animated.View
entering={shouldAnimate ? keyframe.duration(300) : undefined}>
<HeartIconFilled style={s.likeColor} width={size} />
<HeartIconFilled style={{color: t.palette.pink}} width={size} />
</Animated.View>
) : (
<HeartIconOutline
@@ -100,7 +99,7 @@ export function AnimatedLikeIcon({
entering={circle1Keyframe.duration(300)}
style={{
position: 'absolute',
backgroundColor: s.likeColor.color,
backgroundColor: t.palette.pink,
top: 0,
left: 0,
width: size,
+2 -3
View File
@@ -2,7 +2,6 @@ import {useEffect, useRef} from 'react'
import {View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated'
import {s} from '#/lib/styles'
import {useTheme} from '#/alf'
import {
Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled,
@@ -74,7 +73,7 @@ export function AnimatedLikeIcon({
{isLiked ? (
// @ts-expect-error is div
<View ref={likeIconRef}>
<HeartIconFilled style={s.likeColor} width={size} />
<HeartIconFilled style={{color: t.palette.pink}} width={size} />
</View>
) : (
<HeartIconOutline
@@ -87,7 +86,7 @@ export function AnimatedLikeIcon({
ref={circle1Ref}
style={{
position: 'absolute',
backgroundColor: s.likeColor.color,
backgroundColor: t.palette.pink,
top: 0,
left: 0,
width: size,
+1 -1
View File
@@ -1,5 +1,5 @@
import {useMemo} from 'react'
import {useNavigation} from '@react-navigation/core'
import {useNavigation} from '@react-navigation/native'
import {useDedupe} from '#/lib/hooks/useDedupe'
import {type NavigationProp} from '#/lib/routes/types'
+1 -1
View File
@@ -1,5 +1,5 @@
import {useEffect, useMemo, useState} from 'react'
import {type EventArg, useNavigation} from '@react-navigation/core'
import {type EventArg, useNavigation} from '@react-navigation/native'
if ('scrollRestoration' in history) {
// Tell the brower not to mess with the scroll.
+10
View File
@@ -0,0 +1,10 @@
export function Provider({children}: {children: React.ReactNode}) {
return children
}
export function useHotkeysContext() {
return {
enableScope: () => {},
disableScope: () => {},
}
}
+75
View File
@@ -0,0 +1,75 @@
import {useLingui} from '@lingui/react/macro'
import {
HotkeysProvider,
useHotkeys,
useHotkeysContext,
} from 'react-hotkeys-hook'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {emitFocusSearch} from '#/state/events'
import {useSession} from '#/state/session'
enum Hotkeys {
OPEN_COMPOSER = 'n',
FOCUS_SEARCH = 'slash',
}
export function Provider({children}: React.PropsWithChildren<unknown>) {
return (
<HotkeysProvider initiallyActiveScopes={['global']}>
<KeyboardShortcuts>{children}</KeyboardShortcuts>
</HotkeysProvider>
)
}
export {useHotkeysContext}
function KeyboardShortcuts({children}: React.PropsWithChildren<unknown>) {
useKeyboardShortcuts()
return children
}
function useKeyboardShortcuts() {
const {openComposer} = useOpenComposer()
const {hasSession} = useSession()
const {t: l} = useLingui()
const shouldIgnore = (requiresSession: boolean = false) => {
if (requiresSession && !hasSession) {
return true
}
return false
}
const handleKey = (
callback: () => void,
options?: {requiresSession?: boolean},
) => {
if (shouldIgnore(options?.requiresSession)) {
return
}
callback()
}
useHotkeys(
Hotkeys.OPEN_COMPOSER,
() =>
handleKey(
() => {
openComposer({logContext: 'Other'})
},
{
requiresSession: true,
},
),
{scopes: ['global'], description: l`Compose new post`},
[openComposer],
)
useHotkeys(Hotkeys.FOCUS_SEARCH, () => handleKey(emitFocusSearch), {
scopes: ['global'],
preventDefault: true,
description: l`Focus the search field`,
})
}
+6 -2
View File
@@ -15,7 +15,6 @@ import {
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import * as MediaLibrary from 'expo-media-library'
import * as Sharing from 'expo-sharing'
import {Buffer} from 'buffer'
import {POST_IMG_MAX} from '#/lib/constants'
import {logger} from '#/logger'
@@ -322,7 +321,12 @@ export async function saveBytesToDisk(
bytes: Uint8Array,
type: string,
) {
const encoded = Buffer.from(bytes).toString('base64')
// ideally we'd use `bytes.toBase64()`, but that's only baseline newly available
let binary = ''
for (const byte of bytes) {
binary += String.fromCharCode(byte)
}
const encoded = btoa(binary)
return await saveToDevice(filename, encoded, type)
}
+6 -4
View File
@@ -10,7 +10,7 @@ import {
import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
import {PERSISTED_QUERY_ROOT} from '#/state/queries'
import {isQueryPersisted} from '#/state/queries/util'
import * as env from '#/env'
import {IS_NATIVE, IS_WEB} from '#/env'
@@ -137,8 +137,7 @@ const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehyd
{
shouldDehydrateMutation: (_: any) => false,
shouldDehydrateQuery: query => {
const root = String(query.queryKey[0])
return root === PERSISTED_QUERY_ROOT
return isQueryPersisted(query.queryKey)
},
}
@@ -190,7 +189,10 @@ function QueryProviderInner({
})
useEffect(() => {
if (IS_WEB) {
window.__TANSTACK_QUERY_CLIENT__ = queryClient
// WARNING, BROKEN
// something since v5.32.0 causes OOMs. not important
// so disable for now
// window.__TANSTACK_QUERY_CLIENT__ = queryClient
}
}, [queryClient])
return (
+1 -1
View File
@@ -1,4 +1,4 @@
import {XRPCError} from '@atproto/xrpc'
import {XRPCError} from '@atproto/api'
import {t} from '@lingui/core/macro'
export function cleanError(str: any): string {
+1 -124
View File
@@ -1,9 +1,4 @@
import {
Dimensions,
type StyleProp,
StyleSheet,
type TextStyle,
} from 'react-native'
import {type StyleProp, StyleSheet, type TextStyle} from 'react-native'
import {IS_WEB} from '#/env'
import {type Theme, type TypographyVariant} from './ThemeContext'
@@ -61,14 +56,6 @@ export const colors = {
green5: '#082b03',
unreadNotifBg: '#ebf6ff',
brandBlue: '#0066FF',
like: '#ec4899',
}
export const gradients = {
blueLight: {start: '#5A71FA', end: colors.blue3}, // buttons
blue: {start: '#5E55FB', end: colors.blue3}, // fab
blueDark: {start: '#5F45E0', end: colors.blue3}, // avis, banner
}
/**
@@ -78,57 +65,6 @@ export const s = StyleSheet.create({
// helpers
footerSpacer: {height: 100},
contentContainer: {paddingBottom: 200},
contentContainerExtra: {paddingBottom: 300},
border0: {borderWidth: 0},
border1: {borderWidth: 1},
borderTop1: {borderTopWidth: 1},
borderRight1: {borderRightWidth: 1},
borderBottom1: {borderBottomWidth: 1},
borderLeft1: {borderLeftWidth: 1},
hidden: {display: 'none'},
dimmed: {opacity: 0.5},
// font weights
fw600: {fontWeight: '600'},
bold: {fontWeight: '600'},
fw500: {fontWeight: '600'},
semiBold: {fontWeight: '600'},
fw400: {fontWeight: '400'},
normal: {fontWeight: '400'},
fw300: {fontWeight: '400'},
light: {fontWeight: '400'},
// text decoration
underline: {textDecorationLine: 'underline'},
// font variants
tabularNum: {fontVariant: ['tabular-nums']},
// font sizes
f9: {fontSize: 9},
f10: {fontSize: 10},
f11: {fontSize: 11},
f12: {fontSize: 12},
f13: {fontSize: 13},
f14: {fontSize: 14},
f15: {fontSize: 15},
f16: {fontSize: 16},
f17: {fontSize: 17},
f18: {fontSize: 18},
// line heights
['lh13-1']: {lineHeight: 13},
['lh13-1.3']: {lineHeight: 16.9}, // 1.3 of 13px
['lh14-1']: {lineHeight: 14},
['lh14-1.3']: {lineHeight: 18.2}, // 1.3 of 14px
['lh15-1']: {lineHeight: 15},
['lh15-1.3']: {lineHeight: 19.5}, // 1.3 of 15px
['lh16-1']: {lineHeight: 16},
['lh16-1.3']: {lineHeight: 20.8}, // 1.3 of 16px
['lh17-1']: {lineHeight: 17},
['lh17-1.3']: {lineHeight: 22.1}, // 1.3 of 17px
['lh18-1']: {lineHeight: 18},
['lh18-1.3']: {lineHeight: 23.4}, // 1.3 of 18px
// margins
mr2: {marginRight: 2},
@@ -171,74 +107,15 @@ export const s = StyleSheet.create({
pb20: {paddingBottom: 20},
px5: {paddingHorizontal: 5},
// flex
flexRow: {flexDirection: 'row'},
flexCol: {flexDirection: 'column'},
flex1: {flex: 1},
flexGrow1: {flexGrow: 1},
alignCenter: {alignItems: 'center'},
alignBaseline: {alignItems: 'baseline'},
justifyCenter: {justifyContent: 'center'},
// position
absolute: {position: 'absolute'},
// dimensions
w100pct: {width: '100%'},
h100pct: {height: '100%'},
hContentRegion: IS_WEB ? {minHeight: '100%'} : {height: '100%'},
window: {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
},
// text align
textLeft: {textAlign: 'left'},
textCenter: {textAlign: 'center'},
textRight: {textAlign: 'right'},
// colors
white: {color: colors.white},
black: {color: colors.black},
gray1: {color: colors.gray1},
gray2: {color: colors.gray2},
gray3: {color: colors.gray3},
gray4: {color: colors.gray4},
gray5: {color: colors.gray5},
blue1: {color: colors.blue1},
blue2: {color: colors.blue2},
blue3: {color: colors.blue3},
blue4: {color: colors.blue4},
blue5: {color: colors.blue5},
red1: {color: colors.red1},
red2: {color: colors.red2},
red3: {color: colors.red3},
red4: {color: colors.red4},
red5: {color: colors.red5},
pink1: {color: colors.pink1},
pink2: {color: colors.pink2},
pink3: {color: colors.pink3},
pink4: {color: colors.pink4},
pink5: {color: colors.pink5},
purple1: {color: colors.purple1},
purple2: {color: colors.purple2},
purple3: {color: colors.purple3},
purple4: {color: colors.purple4},
purple5: {color: colors.purple5},
green1: {color: colors.green1},
green2: {color: colors.green2},
green3: {color: colors.green3},
green4: {color: colors.green4},
green5: {color: colors.green5},
brandBlue: {color: colors.brandBlue},
likeColor: {color: colors.like},
})
export function lh(
+31 -10
View File
@@ -1,18 +1,17 @@
import {useCallback, useContext, useEffect, useMemo, useState} from 'react'
import {LayoutAnimation, Platform} from 'react-native'
import {getLocales} from 'expo-localization'
import {
isTranslationSupported,
onTranslateTask,
} from '@bsky.app/expo-translate-text'
import {onTranslateTask} from '@bsky.app/expo-translate-text'
import {type TranslationTaskResult} from '@bsky.app/expo-translate-text/build/ExpoTranslateText.types'
import {useLingui} from '@lingui/react/macro'
import {useFocusEffect} from '@react-navigation/native'
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
import {codeToLanguageName} from '#/locale/helpers'
import {logger} from '#/logger'
import {useLanguagePrefs} from '#/state/preferences'
import {useAnalytics} from '#/analytics'
import {IS_ANDROID, IS_IOS} from '#/env'
import {IS_ANDROID, IS_IOS, IS_TRANSLATION_SUPPORTED} from '#/env'
import {Context} from './context'
import {
type ContextType,
@@ -25,6 +24,11 @@ import {guessLanguage} from './utils'
export * from './types'
export * from './utils'
const E_SAME_AS_SOURCE_LANGUAGE =
'Translation result is the same as the source text.'
const E_EMPTY_RESULT = 'Translation result is empty.'
const E_INVALID_SOURCE_LANGUAGE = 'Invalid source language'
/**
* Attempts on-device translation via @bsky.app/expo-translate-text.
* Uses a lazy import to avoid crashing if the native module isn't linked into
@@ -80,11 +84,11 @@ async function attemptTranslation(
typeof result.translatedTexts === 'string' ? result.translatedTexts : ''
if (translatedText === input) {
throw new Error('Translation result is the same as the source text.')
throw new Error(E_SAME_AS_SOURCE_LANGUAGE)
}
if (translatedText === '') {
throw new Error('Translation result is empty.')
throw new Error(E_EMPTY_RESULT)
}
return {
@@ -159,6 +163,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
>({})
const [refCounts, setRefCounts] = useState<Record<string, number>>({})
const ax = useAnalytics()
const langPrefs = useLanguagePrefs()
const {t: l} = useLingui()
const googleTranslate = useGoogleTranslate()
@@ -235,7 +240,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
googleTranslate: shouldForceGoogleTranslate,
})
if (shouldForceGoogleTranslate || !isTranslationSupported()) {
if (shouldForceGoogleTranslate || !IS_TRANSLATION_SUPPORTED) {
await googleTranslate(
text,
expectedTargetLanguage,
@@ -280,7 +285,8 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
postLanguages: possibleSourceLanguages,
},
}))
} catch (e) {
} catch (err) {
const e = err as Error
logger.error('Failed to translate text on device', {safeMessage: e})
// On-device translation failed (language pack missing or user
// dismissed the download prompt).
@@ -295,6 +301,21 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
textLength: text.length,
})
let errorMessage = l`Device failed to translate :(`
if (e.message === E_SAME_AS_SOURCE_LANGUAGE) {
errorMessage = l`Translation to the same language is unavailable on your device.`
}
if (e.message === E_EMPTY_RESULT) {
errorMessage = l`No translation received from your device.`
}
if (
expectedSourceLanguage &&
e.message.includes(E_INVALID_SOURCE_LANGUAGE)
) {
errorMessage = l`${codeToLanguageName(
expectedSourceLanguage,
langPrefs.appLanguage,
)} is not supported by your device.`
}
if (!IS_ANDROID) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
}
@@ -304,7 +325,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
}))
}
},
[ax, googleTranslate, l],
[ax, googleTranslate, l, langPrefs.appLanguage],
)
const ctx = useMemo(
+90 -72
View File
@@ -182,6 +182,11 @@ msgstr ""
msgid "{0} is not available"
msgstr ""
#. placeholder {0}: codeToLanguageName( expectedSourceLanguage, langPrefs.appLanguage, )
#: src/lib/translation/index.tsx:314
msgid "{0} is not supported by your device."
msgstr "{0} is not supported by your device."
#. placeholder {0}: formatCount(i18n, JOINED_THIS_WEEK)
#: src/screens/StarterPack/StarterPackLandingScreen.tsx:232
msgid "{0} joined this week"
@@ -852,8 +857,8 @@ msgstr ""
#: src/view/com/composer/GifAltText.tsx:76
#: src/view/com/composer/GifAltText.tsx:150
#: src/view/com/composer/GifAltText.tsx:217
#: src/view/com/composer/photos/Gallery.tsx:194
#: src/view/com/composer/photos/Gallery.tsx:241
#: src/view/com/composer/photos/Gallery.tsx:196
#: src/view/com/composer/photos/Gallery.tsx:243
#: src/view/com/composer/photos/ImageAltTextDialog.tsx:95
#: src/view/com/composer/photos/ImageAltTextDialog.tsx:103
msgid "Add alt text"
@@ -905,8 +910,8 @@ msgstr ""
msgid "Add media to post"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:522
#: src/components/moderation/ReportDialog/index.tsx:526
#: src/components/moderation/ReportDialog/index.tsx:532
#: src/components/moderation/ReportDialog/index.tsx:536
msgid "Add more details (optional)"
msgstr ""
@@ -997,7 +1002,7 @@ msgstr ""
msgid "Additional details (limit 1000 characters)"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:540
#: src/components/moderation/ReportDialog/index.tsx:550
msgid "Additional details (limit 300 characters)"
msgstr ""
@@ -1157,7 +1162,7 @@ msgstr ""
#: src/components/images/Gallery.tsx:120
#: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:94
#: src/view/com/composer/GifAltText.tsx:100
#: src/view/com/composer/photos/Gallery.tsx:212
#: src/view/com/composer/photos/Gallery.tsx:214
msgid "ALT"
msgstr ""
@@ -1176,7 +1181,7 @@ msgid "Alt Text"
msgstr ""
#: src/view/com/composer/GifAltText.tsx:105
#: src/view/com/composer/photos/Gallery.tsx:124
#: src/view/com/composer/photos/Gallery.tsx:125
msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone."
msgstr ""
@@ -1827,20 +1832,20 @@ msgstr ""
msgid "Browse custom feeds"
msgstr ""
#: src/components/FeedInterstitials.tsx:631
#: src/components/FeedInterstitials.tsx:627
msgid "Browse more accounts"
msgstr ""
#: src/components/FeedInterstitials.tsx:761
#: src/components/FeedInterstitials.tsx:757
msgid "Browse more feeds on the Explore page"
msgstr ""
#: src/components/FeedInterstitials.tsx:742
#: src/components/FeedInterstitials.tsx:745
#: src/components/FeedInterstitials.tsx:738
#: src/components/FeedInterstitials.tsx:741
msgid "Browse more suggestions"
msgstr ""
#: src/components/FeedInterstitials.tsx:770
#: src/components/FeedInterstitials.tsx:766
msgid "Browse more suggestions on the Explore page"
msgstr ""
@@ -1882,7 +1887,7 @@ msgstr ""
#. placeholder {0}: sanitizeHandle(item.feed.creator.handle, '@')
#. placeholder {0}: sanitizeHandle(labeler.creator.handle, '@')
#: src/components/LabelingServiceCard/index.tsx:62
#: src/components/moderation/ReportDialog/index.tsx:843
#: src/components/moderation/ReportDialog/index.tsx:853
#: src/screens/Search/components/StarterPackCard.tsx:107
#: src/screens/Search/Explore.tsx:969
msgid "By {0}"
@@ -1948,7 +1953,7 @@ msgstr ""
#: src/screens/Deactivated.tsx:150
#: src/screens/Profile/Header/EditProfileDialog.tsx:215
#: src/screens/Profile/Header/EditProfileDialog.tsx:223
#: src/screens/Search/Shell.tsx:396
#: src/screens/Search/Shell.tsx:397
#: src/screens/Settings/AppIconSettings/index.tsx:42
#: src/screens/Settings/AppIconSettings/index.tsx:228
#: src/screens/Settings/components/ChangeHandleDialog.tsx:80
@@ -1974,7 +1979,7 @@ msgstr ""
msgid "Cancel reactivation and sign out"
msgstr ""
#: src/screens/Search/Shell.tsx:387
#: src/screens/Search/Shell.tsx:388
msgid "Cancel search"
msgstr ""
@@ -2023,7 +2028,7 @@ msgstr ""
msgid "Change Handle"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:439
#: src/components/moderation/ReportDialog/index.tsx:449
msgid "Change moderation service"
msgstr ""
@@ -2036,11 +2041,11 @@ msgstr ""
msgid "Change password dialog"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:304
#: src/components/moderation/ReportDialog/index.tsx:314
msgid "Change report category"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:384
#: src/components/moderation/ReportDialog/index.tsx:394
msgid "Change report reason"
msgstr ""
@@ -2213,7 +2218,7 @@ msgstr ""
msgid "Clear image cache"
msgstr ""
#: src/components/forms/SearchInput.tsx:69
#: src/components/forms/SearchInput.tsx:87
msgid "Clear search query"
msgstr ""
@@ -2333,7 +2338,7 @@ msgstr ""
msgid "Close dialog"
msgstr ""
#: src/view/shell/index.web.tsx:131
#: src/view/shell/index.web.tsx:129
msgid "Close drawer menu"
msgstr ""
@@ -2425,6 +2430,7 @@ msgstr ""
msgid "Complete the challenge"
msgstr ""
#: src/lib/hotkeys/index.tsx:66
#: src/view/com/feeds/ComposerPrompt.tsx:147
#: src/view/shell/desktop/LeftNav.tsx:575
msgid "Compose new post"
@@ -2913,8 +2919,8 @@ msgstr ""
#. Accessibility label for button to create a moderation report for the selected option
#. placeholder {0}: option.title
#: src/components/moderation/ReportDialog/index.tsx:703
#: src/components/moderation/ReportDialog/index.tsx:749
#: src/components/moderation/ReportDialog/index.tsx:713
#: src/components/moderation/ReportDialog/index.tsx:759
msgid "Create report for {0}"
msgstr ""
@@ -3159,7 +3165,7 @@ msgstr ""
msgid "Developer options"
msgstr ""
#: src/lib/translation/index.tsx:297
#: src/lib/translation/index.tsx:303
msgid "Device failed to translate :("
msgstr "Device failed to translate :("
@@ -3489,7 +3495,7 @@ msgstr ""
#: src/view/com/composer/photos/EditImageDialog.web.tsx:86
#: src/view/com/composer/photos/EditImageDialog.web.tsx:90
#: src/view/com/composer/photos/Gallery.tsx:219
#: src/view/com/composer/photos/Gallery.tsx:221
msgid "Edit image"
msgstr ""
@@ -3760,7 +3766,7 @@ msgstr ""
msgid "Enter your username and password"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:148
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:150
msgid "Enters full screen"
msgstr ""
@@ -4201,7 +4207,7 @@ msgstr ""
#. placeholder {0}: sanitizeHandle(feed.creatorHandle, '@')
#. placeholder {0}: sanitizeHandle(view.creator.handle, '@')
#: src/components/FeedCard.tsx:170
#: src/state/queries/feed.ts:121
#: src/state/queries/feed.ts:118
#: src/view/com/feeds/FeedSourceCard.tsx:151
msgid "Feed by {0}"
msgstr ""
@@ -4344,7 +4350,7 @@ msgstr ""
msgid "Find people to follow"
msgstr ""
#: src/screens/Search/Shell.tsx:529
#: src/screens/Search/Shell.tsx:530
msgid "Find posts, users, and feeds on Bluesky"
msgstr ""
@@ -4388,6 +4394,10 @@ msgstr ""
msgid "Focus code input"
msgstr ""
#: src/lib/hotkeys/index.tsx:73
msgid "Focus the search field"
msgstr "Focus the search field"
#. User is not following this account, click to follow
#: src/components/ProfileCard.tsx:546
#: src/components/ProfileHoverCard/index.web.tsx:495
@@ -5933,7 +5943,7 @@ msgstr ""
msgid "Load new notifications"
msgstr ""
#: src/screens/Profile/ProfileFeed/index.tsx:203
#: src/screens/Profile/ProfileFeed/index.tsx:204
#: src/screens/Profile/Sections/Feed.tsx:117
#: src/screens/ProfileList/FeedSection.tsx:113
#: src/view/com/feeds/FeedPage.tsx:170
@@ -6236,7 +6246,7 @@ msgstr ""
msgid "Music"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:169
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:171
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:97
msgctxt "video"
msgid "Mute"
@@ -6383,8 +6393,8 @@ msgstr ""
msgid "Navigates to your profile"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:335
#: src/components/moderation/ReportDialog/index.tsx:352
#: src/components/moderation/ReportDialog/index.tsx:345
#: src/components/moderation/ReportDialog/index.tsx:362
msgid "Need to report a copyright violation, legal request, or regulatory compliance issue?"
msgstr ""
@@ -6470,7 +6480,7 @@ msgstr ""
msgid "New password"
msgstr ""
#: src/screens/Profile/ProfileFeed/index.tsx:220
#: src/screens/Profile/ProfileFeed/index.tsx:221
#: src/screens/ProfileList/index.tsx:251
#: src/screens/ProfileList/index.tsx:300
#: src/view/screens/Feeds.tsx:553
@@ -6480,13 +6490,9 @@ msgid "New post"
msgstr ""
#: src/view/com/feeds/FeedPage.tsx:181
msgctxt "action"
msgid "New post"
msgstr ""
#: src/view/shell/desktop/LeftNav.tsx:583
msgctxt "action"
msgid "New Post"
msgid "New post"
msgstr ""
#: src/view/com/notifications/NotificationFeedItem.tsx:545
@@ -6712,6 +6718,10 @@ msgstr ""
msgid "No thanks"
msgstr ""
#: src/lib/translation/index.tsx:308
msgid "No translation received from your device."
msgstr "No translation received from your device."
#: src/view/screens/Profile.tsx:500
msgid "No video posts yet"
msgstr ""
@@ -7262,7 +7272,7 @@ msgstr ""
msgid "Password updated!"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:395
msgid "Pause"
msgstr ""
@@ -7377,7 +7387,7 @@ msgstr ""
msgid "Pinned to your feeds"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:396
msgid "Play"
msgstr ""
@@ -7404,7 +7414,7 @@ msgstr ""
msgid "Plays or pauses the GIF"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:154
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:156
msgid "Plays or pauses the video"
msgstr ""
@@ -8121,7 +8131,7 @@ msgstr ""
msgid "Remove from your feeds?"
msgstr ""
#: src/view/com/composer/photos/Gallery.tsx:228
#: src/view/com/composer/photos/Gallery.tsx:230
msgid "Remove image"
msgstr ""
@@ -8329,7 +8339,7 @@ msgid "Report conversation"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:98
#: src/components/moderation/ReportDialog/index.tsx:255
#: src/components/moderation/ReportDialog/index.tsx:265
msgid "Report dialog"
msgstr ""
@@ -8544,7 +8554,7 @@ msgstr ""
#: src/components/dms/MessageItem.tsx:322
#: src/components/Error.tsx:66
#: src/components/Lists.tsx:115
#: src/components/moderation/ReportDialog/index.tsx:289
#: src/components/moderation/ReportDialog/index.tsx:299
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:56
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:59
#: src/components/StarterPack/ProfileStarterPacks.tsx:377
@@ -8566,7 +8576,7 @@ msgstr ""
msgid "Retry"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:286
#: src/components/moderation/ReportDialog/index.tsx:296
#: src/view/screens/Storybook/Admonitions.tsx:61
msgid "Retry loading report options"
msgstr ""
@@ -8722,10 +8732,10 @@ msgid "Scroll to top"
msgstr ""
#: src/components/dialogs/SearchablePeopleList.tsx:515
#: src/components/forms/SearchInput.tsx:33
#: src/components/forms/SearchInput.tsx:35
#: src/components/forms/SearchInput.tsx:51
#: src/components/forms/SearchInput.tsx:53
#: src/screens/Search/Shell.tsx:354
#: src/screens/Search/Shell.tsx:517
#: src/screens/Search/Shell.tsx:518
#: src/view/shell/bottom-bar/BottomBar.tsx:199
msgid "Search"
msgstr ""
@@ -8759,14 +8769,14 @@ msgstr ""
msgid "Search for \"{interestsDisplayName}\" (active)"
msgstr ""
#: src/view/shell/desktop/Search.tsx:130
msgid "Search for \"{query}\""
msgstr ""
#: src/screens/Search/components/AutocompleteResults.tsx:47
msgid "Search for \"{searchText}\""
msgstr ""
#: src/view/shell/desktop/Search.tsx:129
msgid "Search for “{tQuery}”"
msgstr "Search for “{tQuery}”"
#: src/screens/StarterPack/Wizard/index.tsx:552
msgid "Search for feeds that you want to suggest to others."
msgstr ""
@@ -8864,12 +8874,12 @@ msgstr ""
msgid "See jobs at Bluesky"
msgstr ""
#: src/components/FeedInterstitials.tsx:584
#: src/components/FeedInterstitials.tsx:645
#: src/components/FeedInterstitials.tsx:583
#: src/components/FeedInterstitials.tsx:641
msgid "See more"
msgstr ""
#: src/components/FeedInterstitials.tsx:565
#: src/components/FeedInterstitials.tsx:564
msgid "See more suggested profiles"
msgstr ""
@@ -8906,7 +8916,7 @@ msgstr ""
msgid "Select a color"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:374
#: src/components/moderation/ReportDialog/index.tsx:384
msgid "Select a reason"
msgstr ""
@@ -8991,7 +9001,7 @@ msgstr ""
msgid "Select languages"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:424
#: src/components/moderation/ReportDialog/index.tsx:434
msgid "Select moderation service"
msgstr ""
@@ -9090,7 +9100,7 @@ msgstr ""
msgid "Send post to..."
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:814
#: src/components/moderation/ReportDialog/index.tsx:824
msgid "Send report to {title}"
msgstr ""
@@ -9565,7 +9575,7 @@ msgstr ""
msgid "Some of your verifications are invalid."
msgstr ""
#: src/components/FeedInterstitials.tsx:724
#: src/components/FeedInterstitials.tsx:720
msgid "Some other feeds you might like"
msgstr ""
@@ -9593,7 +9603,7 @@ msgid "Something went wrong"
msgstr ""
#: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:139
#: src/components/moderation/ReportDialog/index.tsx:281
#: src/components/moderation/ReportDialog/index.tsx:291
#: src/screens/Deactivated.tsx:86
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:59
#: src/view/screens/Storybook/Admonitions.tsx:56
@@ -9625,7 +9635,7 @@ msgstr ""
msgid "Sorry, we're unable to load account suggestions at this time."
msgstr ""
#: src/App.native.tsx:142
#: src/App.native.tsx:141
#: src/App.web.tsx:119
msgid "Sorry! Your session expired. Please sign in again."
msgstr ""
@@ -9778,9 +9788,9 @@ msgstr ""
msgid "Submit Appeal"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:502
#: src/components/moderation/ReportDialog/index.tsx:563
#: src/components/moderation/ReportDialog/index.tsx:570
#: src/components/moderation/ReportDialog/index.tsx:512
#: src/components/moderation/ReportDialog/index.tsx:573
#: src/components/moderation/ReportDialog/index.tsx:580
msgid "Submit report"
msgstr ""
@@ -10175,7 +10185,7 @@ msgid "There was an issue fetching notifications. Tap here to try again."
msgstr ""
#: src/screens/Search/Explore.tsx:1025
#: src/view/com/posts/PostFeed.tsx:763
#: src/view/com/posts/PostFeed.tsx:773
msgid "There was an issue fetching posts. Tap here to try again."
msgstr ""
@@ -10609,7 +10619,7 @@ msgstr ""
msgid "Toggle to enable or disable adult content"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:171
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:173
msgid "Toggles the sound"
msgstr ""
@@ -10664,6 +10674,10 @@ msgstr "Translating"
msgid "Translating…"
msgstr "Translating…"
#: src/lib/translation/index.tsx:305
msgid "Translation to the same language is unavailable on your device."
msgstr "Translation to the same language is unavailable on your device."
#: src/screens/Settings/ThreadPreferences.tsx:87
#: src/screens/Settings/ThreadPreferences.tsx:92
msgid "Tree view"
@@ -10843,7 +10857,7 @@ msgstr ""
msgid "Unfollows the user"
msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:486
#: src/components/moderation/ReportDialog/index.tsx:496
msgid "Unfortunately, none of your subscribed labelers supports this report type."
msgstr ""
@@ -10882,7 +10896,7 @@ msgstr ""
msgid "Unmute"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:170
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:96
msgctxt "video"
msgid "Unmute"
@@ -10981,6 +10995,10 @@ msgstr ""
msgid "Unsubscribed from list"
msgstr ""
#: src/view/com/composer/text-input/TextInput.tsx:131
msgid "Unsupported clipboard content"
msgstr "Unsupported clipboard content"
#: src/view/com/composer/Composer.tsx:1369
msgid "Unsupported video type: {mimeType}"
msgstr ""
@@ -11136,7 +11154,7 @@ msgid "User list by {0}"
msgstr ""
#. placeholder {0}: sanitizeHandle(view.creator.handle, '@')
#: src/state/queries/feed.ts:162
#: src/state/queries/feed.ts:159
msgid "User List by {0}"
msgstr ""
@@ -11295,8 +11313,8 @@ msgstr ""
msgid "Version {0}"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:86
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:147
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:87
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:149
msgid "Video"
msgstr ""
@@ -11344,7 +11362,7 @@ msgid "Video uploaded"
msgstr ""
#. placeholder {0}: embed.alt
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:86
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:87
msgid "Video: {0}"
msgstr ""
@@ -12522,7 +12540,7 @@ msgid "Your reply was sent"
msgstr ""
#. placeholder {0}: state.selectedLabeler?.creator.displayName
#: src/components/moderation/ReportDialog/index.tsx:513
#: src/components/moderation/ReportDialog/index.tsx:523
msgid "Your report will be sent to <0>{0}</0>."
msgstr ""
+2 -1
View File
@@ -52,8 +52,9 @@ import {atoms as a, native, platform, useBreakpoints, web} from '#/alf'
import * as Layout from '#/components/Layout'
import {ListFooter} from '#/components/Lists'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
const PARENT_CHUNK_SIZE = 20
const PARENT_CHUNK_SIZE = IS_NATIVE ? 5 : 20
const CHILDREN_CHUNK_SIZE = 50
export function PostThread({uri}: {uri: string}) {
@@ -185,6 +185,7 @@ export function ProfileFeedScreenInner({
<FeedFeedbackProvider value={feedFeedback}>
<PostFeed
enabled
feed={feed}
feedParams={feedParams}
pollInterval={60e3}
+1
View File
@@ -380,6 +380,7 @@ export function SearchScreenShell({
inputPlaceholder ?? l`Search for posts, users, or feeds`
}
hitSlop={{...HITSLOP_20, top: 0}}
hotkey={true}
/>
</View>
{showAutocomplete && (
+1 -1
View File
@@ -5,7 +5,7 @@ import {
type AppBskyFeedDefs,
} from '@atproto/api'
import {type QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
import {batchedUpdates} from '#/lib/batchedUpdates'
import {findAllPostsInQueryData as findAllPostsInBookmarksQueryData} from '#/state/queries/bookmarks/useBookmarksQuery'
+1 -1
View File
@@ -1,7 +1,7 @@
import {useEffect, useMemo, useState} from 'react'
import {type AppBskyActorDefs, type AppBskyNotificationDefs} from '@atproto/api'
import {type QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
import {batchedUpdates} from '#/lib/batchedUpdates'
import {findAllProfilesInQueryData as findAllProfilesInActivitySubscriptionsQueryData} from '#/state/queries/activity-subscriptions'
+18 -8
View File
@@ -7,6 +7,7 @@ import {
useState,
} from 'react'
import {useHotkeysContext} from '#/lib/hotkeys'
import {type DialogControlRefProps} from '#/components/Dialog'
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
import {IS_WEB} from '#/env'
@@ -62,6 +63,7 @@ export function useDialogFullyExpandedCountContext() {
export function Provider({children}: React.PropsWithChildren<{}>) {
const [fullyExpandedCount, setFullyExpandedCount] = useState(0)
const {disableScope, enableScope} = useHotkeysContext()
const activeDialogs = useRef<
Map<string, React.MutableRefObject<DialogControlRefProps>>
@@ -77,18 +79,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return openDialogs.current.size > 0
} else {
BottomSheetNativeComponent.dismissAll()
void BottomSheetNativeComponent.dismissAll()
return false
}
}, [])
const setDialogIsOpen = useCallback((id: string, isOpen: boolean) => {
if (isOpen) {
openDialogs.current.add(id)
} else {
openDialogs.current.delete(id)
}
}, [])
const setDialogIsOpen = useCallback(
(id: string, isOpen: boolean) => {
if (isOpen) {
openDialogs.current.add(id)
} else {
openDialogs.current.delete(id)
}
if (openDialogs.current.size > 0) {
disableScope('global')
} else {
enableScope('global')
}
},
[disableScope, enableScope],
)
const context = useMemo<IDialogContext>(
() => ({
+9 -1
View File
@@ -1,4 +1,4 @@
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
type UnlistenFn = () => void
@@ -45,3 +45,11 @@ export function listenPostCreated(fn: () => void): UnlistenFn {
emitter.on('post-created', fn)
return () => emitter.off('post-created', fn)
}
export function emitFocusSearch() {
emitter.emit('focus-search')
}
export function listenFocusSearch(fn: () => void): UnlistenFn {
emitter.on('focus-search', fn)
return () => emitter.off('focus-search', fn)
}
+1 -1
View File
@@ -7,7 +7,7 @@ import {
type GestureUpdateEvent,
type PanGestureHandlerEventPayload,
} from 'react-native-gesture-handler'
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
export type GlobalGestureEvents = {
begin: GestureStateChangeEvent<PanGestureHandlerEventPayload>
+11 -1
View File
@@ -1,7 +1,8 @@
import {createContext, useContext, useMemo, useState} from 'react'
import {createContext, useContext, useEffect, useMemo, useState} from 'react'
import {nanoid} from 'nanoid/non-secure'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useHotkeysContext} from '#/lib/hotkeys'
import {type ImageSource} from '#/view/com/lightbox/ImageViewing/@types'
export type Lightbox = {
@@ -28,6 +29,15 @@ LightboxControlContext.displayName = 'LightboxControlContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [activeLightbox, setActiveLightbox] = useState<Lightbox | null>(null)
const {disableScope, enableScope} = useHotkeysContext()
useEffect(() => {
if (activeLightbox) {
disableScope('global')
} else {
enableScope('global')
}
}, [activeLightbox, disableScope, enableScope])
const openLightbox = useNonReactiveCallback(
(lightbox: Omit<Lightbox, 'id'>) => {
+2 -2
View File
@@ -5,8 +5,8 @@ import {
type ChatBskyConvoGetLog,
type ChatBskyConvoSendMessage,
} from '@atproto/api'
import {XRPCError} from '@atproto/xrpc'
import EventEmitter from 'eventemitter3'
import {XRPCError} from '@atproto/api'
import {EventEmitter} from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure'
import {networkRetry} from '#/lib/async/retry'
+1 -1
View File
@@ -1,5 +1,5 @@
import {type BskyAgent, type ChatBskyConvoGetLog} from '@atproto/api'
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure'
import {networkRetry} from '#/lib/async/retry'
+11 -1
View File
@@ -1,6 +1,7 @@
import {createContext, useContext, useMemo, useState} from 'react'
import {createContext, useContext, useEffect, useMemo, useState} from 'react'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useHotkeysContext} from '#/lib/hotkeys'
export interface UserAddRemoveListsModal {
name: 'user-add-remove-lists'
@@ -47,6 +48,15 @@ ModalControlContext.displayName = 'ModalControlContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [activeModals, setActiveModals] = useState<Modal[]>([])
const {disableScope, enableScope} = useHotkeysContext()
useEffect(() => {
if (activeModals.length > 0) {
disableScope('global')
} else {
enableScope('global')
}
}, [activeModals.length, disableScope, enableScope])
const openModal = useNonReactiveCallback((modal: Modal) => {
setActiveModals(modals => [...modals, modal])
+1 -1
View File
@@ -1,4 +1,4 @@
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
import BroadcastChannel from '#/lib/broadcast'
import {logger} from '#/logger'
+18 -11
View File
@@ -22,13 +22,10 @@ import {
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {
PERSISTED_QUERY_GCTIME,
PERSISTED_QUERY_ROOT,
STALE,
} from '#/state/queries'
import {GCTIME, STALE} from '#/state/queries'
import {RQKEY as listQueryKey} from '#/state/queries/list'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {createQueryKey} from '#/state/queries/util'
import {useAgent, useSession} from '#/state/session'
import {router} from '#/routes'
import {useModerationOpts} from '../preferences/moderation-opts'
@@ -416,10 +413,20 @@ const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = {
contentMode: undefined,
}
const createPinnedFeedInfosQueryKeyRoot = (
const createPinnedFeedInfosQueryKey = (
kind: 'pinned' | 'saved',
feedUris: string[],
) => [PERSISTED_QUERY_ROOT, 'feed-info', kind, feedUris]
) =>
createQueryKey(
'feed-info',
{
kind,
feedUris,
},
{
persistedVersion: 1,
},
)
export function usePinnedFeedsInfos() {
const {hasSession} = useSession()
@@ -428,11 +435,11 @@ export function usePinnedFeedsInfos() {
const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? []
return useQuery({
queryKey: createPinnedFeedInfosQueryKeyRoot(
queryKey: createPinnedFeedInfosQueryKey(
'pinned',
pinnedItems.map(f => f.value),
),
gcTime: PERSISTED_QUERY_GCTIME,
gcTime: GCTIME.INFINITY,
staleTime: STALE.INFINITY,
enabled: !isLoadingPrefs,
queryFn: async () => {
@@ -536,11 +543,11 @@ export function useSavedFeeds() {
const queryClient = useQueryClient()
return useQuery({
queryKey: createPinnedFeedInfosQueryKeyRoot(
queryKey: createPinnedFeedInfosQueryKey(
'saved',
savedItems.map(f => f.value),
),
gcTime: PERSISTED_QUERY_GCTIME,
gcTime: GCTIME.INFINITY,
staleTime: STALE.INFINITY,
enabled: !isLoadingPrefs,
placeholderData: previousData => {
+3 -19
View File
@@ -19,22 +19,6 @@ export const STALE = {
INFINITY: Infinity,
}
/**
* Root key for persisted queries.
*
* If the `querykey` of your query uses this at index 0, it will be
* persisted automatically by the `PersistQueryClientProvider` in
* `#/lib/react-query.tsx`.
*
* Be careful when using this, since it will change the query key and may
* break any cases where we call `invalidateQueries` or `refetchQueries`
* with the old key.
*
* Also, only use this for queries that are safe to persist between
* app launches (like user preferences).
*
* Note that for queries that are persisted, it is recommended to extend
* the `gcTime` to a longer duration, otherwise it'll get busted
*/
export const PERSISTED_QUERY_ROOT = 'PERSISTED'
export const PERSISTED_QUERY_GCTIME = Infinity
export const GCTIME = {
INFINITY: Infinity,
}
+6 -12
View File
@@ -3,15 +3,12 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {z} from 'zod'
import {MAX_LABELERS} from '#/lib/constants'
import {
PERSISTED_QUERY_GCTIME,
PERSISTED_QUERY_ROOT,
STALE,
} from '#/state/queries'
import {GCTIME, STALE} from '#/state/queries'
import {
preferencesQueryKey,
usePreferencesQuery,
} from '#/state/queries/preferences'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
const labelerInfoQueryKeyRoot = 'labeler-info'
@@ -26,11 +23,8 @@ export const labelersInfoQueryKey = (dids: string[]) => [
dids.slice().sort(),
]
const persistedLabelersDetailedInfoQueryKey = (dids: string[]) => [
PERSISTED_QUERY_ROOT,
'labelers-detailed-info',
dids,
]
const createLabelersDetailedInfoQueryKey = (dids: string[]) =>
createQueryKey('labelers-detailed-info', {dids}, {persistedVersion: 1})
export function useLabelerInfoQuery({
did,
@@ -69,8 +63,8 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
const agent = useAgent()
return useQuery({
enabled: !!dids.length,
queryKey: persistedLabelersDetailedInfoQueryKey(dids),
gcTime: PERSISTED_QUERY_GCTIME,
queryKey: createLabelersDetailedInfoQueryKey(dids),
gcTime: GCTIME.INFINITY,
staleTime: STALE.MINUTES.ONE,
queryFn: async () => {
const res = await agent.app.bsky.labeler.getServices({
+1 -1
View File
@@ -12,7 +12,7 @@ import {
} from 'react'
import {AppState} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
import BroadcastChannel from '#/lib/broadcast'
import {resetBadgeCount} from '#/lib/notifications/notifications'
+8 -7
View File
@@ -9,11 +9,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {replaceEqualDeep} from '#/lib/functions'
import {getAge} from '#/lib/strings/time'
import {
PERSISTED_QUERY_GCTIME,
PERSISTED_QUERY_ROOT,
STALE,
} from '#/state/queries'
import {GCTIME, STALE} from '#/state/queries'
import {
DEFAULT_HOME_FEED_PREFS,
DEFAULT_LOGGED_OUT_PREFERENCES,
@@ -23,6 +19,7 @@ import {
type ThreadViewPreferences,
type UsePreferencesQueryResponse,
} from '#/state/queries/preferences/types'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {saveLabelers} from '#/state/session/agent-config'
import {useAgeAssurance} from '#/ageAssurance'
@@ -33,7 +30,11 @@ export * from '#/state/queries/preferences/const'
export * from '#/state/queries/preferences/moderation'
export * from '#/state/queries/preferences/types'
export const preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences']
export const preferencesQueryKey = createQueryKey(
'getPreferences',
{},
{persistedVersion: 1},
)
export function usePreferencesQuery() {
const agent = useAgent()
@@ -44,7 +45,7 @@ export function usePreferencesQuery() {
structuralSharing: replaceEqualDeep,
refetchOnWindowFocus: true,
queryKey: preferencesQueryKey,
gcTime: PERSISTED_QUERY_GCTIME,
gcTime: GCTIME.INFINITY,
queryFn: async () => {
if (!agent.did) {
return DEFAULT_LOGGED_OUT_PREFERENCES
+1
View File
@@ -99,6 +99,7 @@ export function useProfilesQuery({
}) {
const agent = useAgent()
return useQuery({
enabled: handles.length > 0,
staleTime: STALE.MINUTES.FIVE,
queryKey: profilesQueryKey(handles),
queryFn: async () => {
+32 -26
View File
@@ -1,9 +1,5 @@
import {AtUri} from '@atproto/api'
import {
type QueryClient,
useQuery,
type UseQueryResult,
} from '@tanstack/react-query'
import {AtUri, type BskyAgent} from '@atproto/api'
import {type QueryClient, queryOptions, useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
@@ -12,26 +8,12 @@ import {useUnstableProfileViewCache} from './profile'
const RQKEY_ROOT = 'resolved-did'
export const RQKEY = (didOrHandle: string) => [RQKEY_ROOT, didOrHandle]
type UriUseQueryResult = UseQueryResult<{did: string; uri: string}, Error>
export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
const urip = new AtUri(uri || '')
const res = useResolveDidQuery(urip.host)
if (res.data) {
// @ts-expect-error TODO new-sdk-migration
urip.host = res.data
return {
...res,
data: {did: urip.host, uri: urip.toString()},
} as UriUseQueryResult
}
return res as UriUseQueryResult
}
export function useResolveDidQuery(didOrHandle: string | undefined) {
const agent = useAgent()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery<string, Error>({
const resolvedDidQueryOptions = (
agent: BskyAgent,
getUnstableProfile: (did: string) => {did: string} | undefined,
didOrHandle: string | undefined,
) =>
queryOptions({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY(didOrHandle ?? ''),
queryFn: async () => {
@@ -50,6 +32,30 @@ export function useResolveDidQuery(didOrHandle: string | undefined) {
},
enabled: !!didOrHandle,
})
export function useResolveUriQuery(uri: string | undefined) {
const urip = new AtUri(uri || '')
const host = urip.host
const agent = useAgent()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery({
...resolvedDidQueryOptions(agent, getUnstableProfile, host),
select: did => ({
did,
uri: AtUri.make(did, urip.collection, urip.rkey).toString(),
}),
})
}
export function useResolveDidQuery(didOrHandle: string | undefined) {
const agent = useAgent()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery(
resolvedDidQueryOptions(agent, getUnstableProfile, didOrHandle),
)
}
export function precacheResolvedUri(
+55
View File
@@ -14,6 +14,61 @@ import {
import * as bsky from '#/types/bsky'
export type StructuredQueryKey<T extends Record<string, unknown>> = readonly [
string,
T,
{
persistedVersion?: number
},
]
/**
* Helper method to ensure consistent query keys and key ordering
*/
export function createQueryKey<T extends Record<string, unknown>>(
/**
* The query key root. All queries must have a root.
*/
root: string,
/**
* Any arguments the query depends on, and if changed, should result in the query being refetched.
*/
args: T,
options: {
/**
* If provided, this indicates that the query is persisted and the version
* of the persisted query format.
*
* This is used to ensure that when we make breaking changes to the
* persisted query format, we can increment the version and avoid trying to
* read old persisted queries with the new format.
*
* If you're persisting your queries, you probably want to set `gcTime:
* GCTIME.INFINITY` for this query, otherwise it'll get busted immediately
* after being persisted.
*/
persistedVersion?: number
} = {},
): StructuredQueryKey<T> {
return [root, args, options] as const
}
export function isQueryPersisted(
queryKey: QueryKey,
): queryKey is StructuredQueryKey<Record<string, unknown>> {
return (
Array.isArray(queryKey) &&
queryKey.length === 3 &&
typeof queryKey[0] === 'string' &&
typeof queryKey[1] === 'object' &&
queryKey[1] !== null &&
typeof queryKey[2] === 'object' &&
queryKey[2] !== null &&
'persistedVersion' in queryKey[2] &&
typeof queryKey[2].persistedVersion === 'number'
)
}
export async function truncateAndInvalidate<T = any>(
queryClient: QueryClient,
queryKey: QueryKey,
@@ -1,77 +0,0 @@
import {useEffect} from 'react'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useDialogStateContext} from '#/state/dialogs'
import {useLightbox} from '#/state/lightbox'
import {useModals} from '#/state/modals'
import {useSession} from '#/state/session'
import {useIsDrawerOpen} from '#/state/shell/drawer-open'
/**
* Based on {@link https://github.com/jaywcjlove/hotkeys-js/blob/b0038773f3b902574f22af747f3bb003a850f1da/src/index.js#L51C1-L64C2}
*/
function shouldIgnore(event: KeyboardEvent) {
const target: any = event.target || event.srcElement
if (!target) return false
const {tagName} = target
if (!tagName) return false
const isInput =
tagName === 'INPUT' &&
![
'checkbox',
'radio',
'range',
'button',
'file',
'reset',
'submit',
'color',
].includes(target.type)
// ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select>
if (
target.isContentEditable ||
((isInput || tagName === 'TEXTAREA' || tagName === 'SELECT') &&
!target.readOnly)
) {
return true
}
return false
}
export function useComposerKeyboardShortcut() {
const {openComposer} = useOpenComposer()
const {openDialogs} = useDialogStateContext()
const {isModalActive} = useModals()
const {activeLightbox} = useLightbox()
const isDrawerOpen = useIsDrawerOpen()
const {hasSession} = useSession()
useEffect(() => {
if (!hasSession) {
return
}
function handler(event: KeyboardEvent) {
if (shouldIgnore(event)) return
if (
openDialogs?.current.size > 0 ||
isModalActive ||
activeLightbox ||
isDrawerOpen
)
return
if (event.key === 'n' || event.key === 'N') {
openComposer({logContext: 'Other'})
}
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [
openComposer,
isModalActive,
openDialogs,
activeLightbox,
isDrawerOpen,
hasSession,
])
}
+15 -1
View File
@@ -1,5 +1,7 @@
import {createContext, useContext, useState} from 'react'
import {useHotkeysContext} from '#/lib/hotkeys'
type StateContext = boolean
type SetContext = (v: boolean) => void
@@ -10,10 +12,22 @@ setContext.displayName = 'DrawerOpenSetContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = useState(false)
const {disableScope, enableScope} = useHotkeysContext()
const setDrawerOpen = (open: boolean) => {
if (open) {
disableScope('global')
} else {
enableScope('global')
}
setState(open)
}
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setState}>{children}</setContext.Provider>
<setContext.Provider value={setDrawerOpen}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}
+1 -1
View File
@@ -101,7 +101,7 @@ export function GifAltTextDialogLoaded({
</Text>
</TouchableOpacity>
<Admonition type="tip" style={[a.mt_sm]}>
<Admonition type="info" style={[a.mt_sm]}>
<Trans>
Alt text describes images for blind and low-vision users, and helps
give context to everyone.
+8 -6
View File
@@ -120,12 +120,14 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
)
})}
</View>
<Admonition type="tip" style={[a.mt_sm]}>
<Trans>
Alt text describes images for blind and low-vision users, and helps
give context to everyone.
</Trans>
</Admonition>
{images.some(image => !image.alt) && (
<Admonition type="info" style={[a.mt_sm]}>
<Trans>
Alt text describes images for blind and low-vision users, and helps
give context to everyone.
</Trans>
</Admonition>
)}
</>
) : null
}
+52 -52
View File
@@ -8,21 +8,17 @@ import {
import {
type NativeSyntheticEvent,
Text as RNText,
TextInput as RNTextInput,
type TextInputSelectionChangeEventData,
View,
} from 'react-native'
import {type PasteEventPayload, TextInputWrapper} from 'expo-paste-input'
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import PasteInput, {
type PastedFile,
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 {useLingui} from '@lingui/react/macro'
import {POST_IMG_MAX} from '#/lib/constants'
import {downloadAndResize} from '#/lib/media/manip'
import {isUriImage} from '#/lib/media/util'
import {cleanError} from '#/lib/strings/errors'
import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip'
import {useTheme} from '#/lib/ThemeContext'
import {
@@ -51,8 +47,9 @@ export function TextInput({
onError,
...props
}: TextInputProps) {
const {t: l} = useLingui()
const {theme: t, fonts} = useAlf()
const textInput = useRef<PasteInputRef>(null)
const textInput = useRef<RNTextInput>(null)
const textInputSelection = useRef<Selection>({start: 0, end: 0})
const theme = useTheme()
const [autocompletePrefix, setAutocompletePrefix] = useState('')
@@ -129,19 +126,21 @@ export function TextInput({
)
const onPaste = useCallback(
async (err: string | undefined, files: PastedFile[]) => {
if (err) {
return onError(cleanError(err))
(payload: PasteEventPayload) => {
if (payload.type === 'unsupported') {
onError(l`Unsupported clipboard content`)
return
}
const uris = files.map(f => f.uri)
const uri = uris.find(isUriImage)
if (uri) {
onPhotoPasted(uri)
if (payload.type === 'images') {
for (const uri of payload.uris) {
if (isUriImage(uri)) {
onPhotoPasted(uri)
}
}
}
},
[onError, onPhotoPasted],
[l, onError, onPhotoPasted],
)
const onSelectionChange = useCallback(
@@ -217,41 +216,42 @@ export function TextInput({
return (
<View style={[a.flex_1, a.pl_md, hasRightPadding && a.pr_4xl]}>
<PasteInput
testID="composerTextInput"
ref={textInput}
onChangeText={onChangeText}
onPaste={onPaste}
onSelectionChange={onSelectionChange}
placeholder={placeholder}
placeholderTextColor={t.atoms.text_contrast_low.color}
keyboardAppearance={theme.colorScheme}
autoFocus={props.autoFocus !== undefined ? props.autoFocus : true}
allowFontScaling
multiline
scrollEnabled={false}
numberOfLines={2}
// Note: should be the default value, but as of v1.104
// it switched to "none" on Android
autoCapitalize="sentences"
{...props}
style={[
inputTextStyle,
a.w_full,
!autocompletePrefix && a.h_full,
{
textAlignVertical: 'top',
minHeight: 60,
includeFontPadding: false,
},
{
borderWidth: 1,
borderColor: 'transparent',
},
props.style,
]}>
{textDecorated}
</PasteInput>
<TextInputWrapper onPaste={onPaste}>
<RNTextInput
testID="composerTextInput"
ref={textInput}
onChangeText={onChangeText}
onSelectionChange={onSelectionChange}
placeholder={placeholder}
placeholderTextColor={t.atoms.text_contrast_low.color}
keyboardAppearance={theme.colorScheme}
autoFocus={props.autoFocus !== undefined ? props.autoFocus : true}
allowFontScaling
multiline
scrollEnabled={false}
numberOfLines={2}
// Note: should be the default value, but as of v1.104
// it switched to "none" on Android
autoCapitalize="sentences"
{...props}
style={[
inputTextStyle,
a.w_full,
!autocompletePrefix && a.h_full,
{
textAlignVertical: 'top',
minHeight: 60,
includeFontPadding: false,
},
{
borderWidth: 1,
borderColor: 'transparent',
},
props.style,
]}>
{textDecorated}
</RNTextInput>
</TextInputWrapper>
<Autocomplete
prefix={autocompletePrefix}
onSelect={onSelectAutocompleteItem}
@@ -1,3 +1,3 @@
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
export const textInputWebEmitter = new EventEmitter()
+1 -1
View File
@@ -582,7 +582,7 @@ function LightboxFooter({
{altText ? (
<View accessibilityRole="button" style={styles.footerText}>
<Text
style={[s.gray3]}
style={{color: colors.gray3}}
numberOfLines={isAltExpanded ? undefined : 3}
selectable
onPress={() => {
+1 -1
View File
@@ -202,7 +202,7 @@ function ListItem({
<View style={styles.listItemContent}>
<Text
type="lg"
style={[s.bold, pal.text]}
style={[{fontWeight: '600'}, pal.text]}
numberOfLines={1}
lineHeight={1.2}>
{sanitizeDisplayName(list.name)}
@@ -272,7 +272,7 @@ let NotificationFeedItem = ({
<HeartIconFilled
size="xl"
style={[
s.likeColor,
{color: t.palette.pink},
// {position: 'relative', top: -4}
]}
/>
+12 -2
View File
@@ -38,6 +38,7 @@ import {
RQKEY,
usePostFeedQuery,
} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useProgressGuide} from '#/state/shell/progress-guide'
import {useSelectedFeed} from '#/state/shell/selected-feed'
@@ -700,13 +701,22 @@ let PostFeed = ({
})
setIsPTRing(true)
try {
await refetch()
await truncateAndInvalidate(queryClient, RQKEY(feed, feedParams))
onHasNew?.(false)
} catch (err) {
logger.error('Failed to refresh posts feed', {message: err})
}
setIsPTRing(false)
}, [ax, refetch, setIsPTRing, onHasNew, feed, feedType, enabled])
}, [
ax,
queryClient,
setIsPTRing,
onHasNew,
feed,
feedParams,
feedType,
enabled,
])
const onEndReached = useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
+2 -2
View File
@@ -60,7 +60,7 @@ export function PostLoadingPlaceholder({
},
]}
/>
<View style={[s.flex1]}>
<View style={[a.flex_1]}>
<LoadingPlaceholder width={100} height={6} style={{marginBottom: 10}} />
<LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} />
<LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} />
@@ -238,7 +238,7 @@ export function FeedLoadingPlaceholder({
height={36}
style={[styles.avatar, {borderRadius: 8}]}
/>
<View style={[s.flex1]}>
<View style={[a.flex_1]}>
<LoadingPlaceholder width={100} height={8} style={[s.mt5, s.mb10]} />
<LoadingPlaceholder width={120} height={8} />
</View>
+1 -1
View File
@@ -7,7 +7,7 @@ import {
withSpring,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import EventEmitter from 'eventemitter3'
import {EventEmitter} from 'eventemitter3'
import {ScrollProvider} from '#/lib/ScrollContext'
import {useMinimalShellMode} from '#/state/shell'
+7 -7
View File
@@ -174,7 +174,7 @@ function NotifsView() {
}
return (
<View style={s.p10}>
<View style={s.flexRow}>
<View style={{flexDirection: 'row'}}>
<Button onPress={triggerPush} label="Trigger Push" />
<Button onPress={triggerToast} label="Trigger Toast" />
<Button onPress={triggerToast2} label="Trigger Toast 2" />
@@ -187,7 +187,7 @@ function PaletteView({palette}: {palette: PaletteColorName}) {
const defaultPal = usePalette('default')
const pal = usePalette(palette)
return (
<View style={[pal.view, pal.border, s.p10, s.mb5, s.border1]}>
<View style={[pal.view, pal.border, s.p10, s.mb5, {borderWidth: 1}]}>
<Text style={[pal.text]}>{palette} colors</Text>
<Text style={[pal.textLight]}>Light text</Text>
<Text style={[pal.link]}>Link text</Text>
@@ -343,15 +343,15 @@ function ButtonsView() {
const buttonStyles = {marginRight: 5}
return (
<View style={[defaultPal.view]}>
<View style={[s.flexRow, s.mb5]}>
<View style={[{flexDirection: 'row'}, s.mb5]}>
<Button type="primary" label="Primary solid" style={buttonStyles} />
<Button type="secondary" label="Secondary solid" style={buttonStyles} />
</View>
<View style={[s.flexRow, s.mb5]}>
<View style={[{flexDirection: 'row'}, s.mb5]}>
<Button type="default" label="Default solid" style={buttonStyles} />
<Button type="inverted" label="Inverted solid" style={buttonStyles} />
</View>
<View style={s.flexRow}>
<View style={{flexDirection: 'row'}}>
<Button
type="primary-outline"
label="Primary outline"
@@ -363,7 +363,7 @@ function ButtonsView() {
style={buttonStyles}
/>
</View>
<View style={s.flexRow}>
<View style={{flexDirection: 'row'}}>
<Button
type="primary-light"
label="Primary light"
@@ -375,7 +375,7 @@ function ButtonsView() {
style={buttonStyles}
/>
</View>
<View style={s.flexRow}>
<View style={{flexDirection: 'row'}}>
<Button
type="default-light"
label="Default light"
+8 -2
View File
@@ -400,10 +400,16 @@ function Btn({
a.rounded_full,
{backgroundColor: t.palette.primary_500},
]}>
<Text style={styles.notificationCountLabel}>{notificationCount}</Text>
<Text
style={styles.notificationCountLabel}
maxFontSizeMultiplier={1.5}>
{notificationCount}
</Text>
</View>
) : hasNew ? (
<View style={[styles.hasNewBadge, a.rounded_full]} />
<View
style={[styles.hasNewBadge, {backgroundColor: t.palette.primary_500}]}
/>
) : null}
</PressableScale>
)
@@ -1,6 +1,5 @@
import {StyleSheet} from 'react-native'
import {colors} from '#/lib/styles'
import {atoms as a} from '#/alf'
export const styles = StyleSheet.create({
@@ -24,8 +23,9 @@ export const styles = StyleSheet.create({
position: 'absolute',
left: '52%',
top: 8,
paddingHorizontal: 4,
paddingBottom: 1,
paddingHorizontal: 5,
paddingTop: 1,
paddingBottom: 2,
borderRadius: 6,
zIndex: 1,
},
@@ -37,8 +37,9 @@ export const styles = StyleSheet.create({
notificationCountLabel: {
fontSize: 12,
fontWeight: '600',
color: colors.white,
color: 'white',
fontVariant: ['tabular-nums'],
includeFontPadding: false,
},
hasNewBadge: {
position: 'absolute',
@@ -47,8 +48,7 @@ export const styles = StyleSheet.create({
top: 10,
width: 8,
height: 8,
backgroundColor: colors.blue3,
borderRadius: 6,
borderRadius: 4,
zIndex: 1,
},
ctrlIcon: {
+3 -1
View File
@@ -313,7 +313,9 @@ const NavItem: React.FC<{
<Text style={styles.notificationCountLabel}>{notificationCount}</Text>
</View>
) : hasNew ? (
<View style={styles.hasNewBadge} />
<View
style={[styles.hasNewBadge, {backgroundColor: t.palette.primary_500}]}
/>
) : null}
</Link>
)
+1 -1
View File
@@ -580,7 +580,7 @@ function ComposeBtn() {
style={[a.rounded_full]}>
<ButtonIcon icon={EditBig} position="left" />
<ButtonText>
<Trans context="action">New Post</Trans>
<Trans context="action">New post</Trans>
</ButtonText>
</Button>
</View>
+1 -1
View File
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/core'
import {useNavigation} from '@react-navigation/native'
import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
import {useKawaiiMode} from '#/state/preferences/kawaii'
+1 -3
View File
@@ -9,7 +9,6 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
import {type NavigationProp} from '#/lib/routes/types'
import {useSession} from '#/state/session'
import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
import {useCloseAllActiveElements} from '#/state/util'
import {Lightbox} from '#/view/com/lightbox/Lightbox'
import {ModalsContainer} from '#/view/com/modals/Modal'
@@ -36,7 +35,7 @@ import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen'
import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay'
import {PassiveAnalytics} from '#/analytics/PassiveAnalytics'
import {FlatNavigator, RoutesContainer} from '#/Navigation'
import {Composer} from './Composer.web'
import {Composer} from './Composer'
import {DrawerContent} from './Drawer'
function ShellInner() {
@@ -45,7 +44,6 @@ function ShellInner() {
const {state: policyUpdateState} = usePolicyUpdateContext()
const welcomeModalControl = useWelcomeModal()
useComposerKeyboardShortcut()
useIntentHandler()
useEffect(() => {
+43 -334
View File
@@ -20,14 +20,14 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@atproto/api@^0.19.3":
version "0.19.6"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.6.tgz#c8fae3d792fe429c900ac0ba2609d60b9a89e28b"
integrity sha512-8L5dZvGaclB52b8msjtgDNx3uLWUY4PELA7KbFyAWBFVasCceE1txdrscCqDCLN+Fff9+Sm07OIjnjHYJXdETA==
"@atproto/api@^0.19.5":
version "0.19.5"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.5.tgz#6388e5d6d3a1693fe04b5f37c705682bac8601d3"
integrity sha512-u6R5TecYJDO8l8QFN09AMuJASYnUkJ4HhYE5hg4/dha/z14a+OAil2/dli/208uM5AHPFLtlnB8kIK9XU5GgQQ==
dependencies:
"@atproto/common-web" "^0.4.19"
"@atproto/lexicon" "^0.6.2"
"@atproto/syntax" "^0.5.3"
"@atproto/syntax" "^0.5.2"
"@atproto/xrpc" "^0.7.7"
await-lock "^2.2.2"
multiformats "^9.9.0"
@@ -73,20 +73,13 @@
multiformats "^9.9.0"
zod "^3.23.8"
"@atproto/syntax@^0.5.0", "@atproto/syntax@^0.5.1":
"@atproto/syntax@^0.5.0", "@atproto/syntax@^0.5.1", "@atproto/syntax@^0.5.2":
version "0.5.2"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.5.2.tgz#d4b32c9feb421ceeb5ade1fa80bc42764d51e52e"
integrity sha512-W41szOnkppoHr0iCUrzL8gy3OD6qmDyp1UvUgmTx2oFQfgbudpz51T/gznesiCcqiUT5obfHdx4PJ+WdlEOE7Q==
dependencies:
tslib "^2.8.1"
"@atproto/syntax@^0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.5.3.tgz#4331d01f63fe56c374dcf95d4432a22b62271a17"
integrity sha512-gzhlHOJHm5KXdCc17fXi1fXM81ccs5jJfNgCui84ay9JGvczxegpYHNqdMlv+iBuhtBzFIjgx6ChjRxN/kO8kQ==
dependencies:
tslib "^2.8.1"
"@atproto/xrpc@^0.7.7":
version "0.7.7"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.7.tgz#c0e3106c854cb9bc7d3129de2f31b8256eb0ed11"
@@ -304,15 +297,6 @@
json5 "^2.2.3"
semver "^6.3.1"
"@babel/eslint-parser@^7.25.1":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.28.6.tgz#6a294a4add732ebe7ded8a8d2792dd03dd81dc3f"
integrity sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==
dependencies:
"@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1"
eslint-visitor-keys "^2.1.0"
semver "^6.3.1"
"@babel/generator@^7.20.5":
version "7.23.6"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e"
@@ -2622,14 +2606,14 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5"
integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1":
"@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1":
version "4.9.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2":
"@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2":
version "4.12.2"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
@@ -3920,12 +3904,6 @@
"@babel/runtime" "^7.20.13"
"@lingui/core" "5.9.2"
"@mattermost/react-native-paste-input@mattermost/react-native-paste-input":
version "0.8.1"
resolved "https://codeload.github.com/mattermost/react-native-paste-input/tar.gz/f260447edc645a817ab1ba7b46d8341d84dba8e9"
dependencies:
semver "7.6.3"
"@messageformat/parser@^5.0.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@messageformat/parser/-/parser-5.1.0.tgz#05e4851c782d633ad735791dd0a68ee65d2a7201"
@@ -3955,13 +3933,6 @@
"@emnapi/runtime" "^1.4.3"
"@tybys/wasm-util" "^0.10.0"
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
integrity sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==
dependencies:
eslint-scope "5.1.1"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -4800,29 +4771,6 @@
serve-static "^1.16.2"
ws "^6.2.3"
"@react-native/eslint-config@^0.81.5":
version "0.81.6"
resolved "https://registry.yarnpkg.com/@react-native/eslint-config/-/eslint-config-0.81.6.tgz#6911df60f250f6462b3b7361190d276bb09c0432"
integrity sha512-Uur91Ss9L8W7omBAqnNe80GNUNn5qQXYuP0xmiKmytGk0ttG9QkcOma4aS4nR3m8O7v0kFA7U3+AD5an526Jsg==
dependencies:
"@babel/core" "^7.25.2"
"@babel/eslint-parser" "^7.25.1"
"@react-native/eslint-plugin" "0.81.6"
"@typescript-eslint/eslint-plugin" "^7.1.1"
"@typescript-eslint/parser" "^7.1.1"
eslint-config-prettier "^8.5.0"
eslint-plugin-eslint-comments "^3.2.0"
eslint-plugin-ft-flow "^2.0.1"
eslint-plugin-jest "^27.9.0"
eslint-plugin-react "^7.30.1"
eslint-plugin-react-hooks "^5.2.0"
eslint-plugin-react-native "^4.0.0"
"@react-native/eslint-plugin@0.81.6":
version "0.81.6"
resolved "https://registry.yarnpkg.com/@react-native/eslint-plugin/-/eslint-plugin-0.81.6.tgz#9c3831a1a3a1204f016ce53b68e99e9269c0115f"
integrity sha512-2wLmnq6l2dualDoAj42P4miwReXt5i7rad31zenhYFBSUNt849y2sroP2I6Yjh0r+UqBdDlf6QN2W+Pdmi6pJw==
"@react-native/gradle-plugin@0.81.5":
version "0.81.5"
resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.81.5.tgz#a58830f38789f6254b64449a17fe57455b589d00"
@@ -5164,7 +5112,7 @@
dependencies:
"@sinonjs/commons" "^3.0.0"
"@tanstack/query-async-storage-persister@^5.25.0":
"@tanstack/query-async-storage-persister@^5.96.2":
version "5.96.2"
resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.96.2.tgz#29423b35f2d8c5f63afbf72475baa7799ffaf022"
integrity sha512-lYJm+TwzOEUVkxCJapLSzRXPzmPpv7Vy3zSB1RXYQ6+vznEgXBqLjn+ZwBRvHpkRda9VXis64wv44rPIi9nCwg==
@@ -5172,11 +5120,6 @@
"@tanstack/query-core" "5.96.2"
"@tanstack/query-persist-client-core" "5.96.2"
"@tanstack/query-core@5.25.0":
version "5.25.0"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.25.0.tgz#e08ed0a9fad34c8005d1a282e57280031ac50cdc"
integrity sha512-vlobHP64HTuSE68lWF1mEhwSRC5Q7gaT+a/m9S+ItuN+ruSOxe1rFnR9j0ACWQ314BPhBEVKfBQ6mHL0OWfdbQ==
"@tanstack/query-core@5.96.2":
version "5.96.2"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.96.2.tgz#766dab253476afd0b27959b66abb606d8d2dd9f5"
@@ -5189,19 +5132,19 @@
dependencies:
"@tanstack/query-core" "5.96.2"
"@tanstack/react-query-persist-client@^5.25.0":
"@tanstack/react-query-persist-client@^5.96.2":
version "5.96.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.96.2.tgz#b47d62fc990a9fd38ddcf4a080d1300ae887e5a0"
integrity sha512-smQ38oVPlnvkG+G7R60IAD9X6azJLRjHEd7twml9XBLYM31ncPDP0tUKy/Gv/4ItVmKTtjZ5VabXpVZxnaWSww==
dependencies:
"@tanstack/query-persist-client-core" "5.96.2"
"@tanstack/react-query@5.25.0":
version "5.25.0"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.25.0.tgz#f4dac794cf10dd956aa56dbbdf67049a5ba2669d"
integrity sha512-u+n5R7mLO7RmeiIonpaCRVXNRWtZEef/aVZ/XGWRPa7trBIvGtzlfo0Ah7ZtnTYfrKEVwnZ/tzRCBcoiqJ/tFw==
"@tanstack/react-query@^5.96.2":
version "5.96.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.96.2.tgz#a164abfb80eb5e7772bbcddfa7240f3fd8d0d7be"
integrity sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==
dependencies:
"@tanstack/query-core" "5.25.0"
"@tanstack/query-core" "5.96.2"
"@testing-library/react-native@^13.2.0":
version "13.2.0"
@@ -5615,11 +5558,6 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
"@types/semver@^7.3.12":
version "7.7.1"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528"
integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==
"@types/send@*":
version "0.17.1"
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301"
@@ -5699,21 +5637,6 @@
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/eslint-plugin@^7.1.1":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz#b16d3cf3ee76bf572fdf511e79c248bdec619ea3"
integrity sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "7.18.0"
"@typescript-eslint/type-utils" "7.18.0"
"@typescript-eslint/utils" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
graphemer "^1.4.0"
ignore "^5.3.1"
natural-compare "^1.4.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/parser@8.58.0":
version "8.58.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.58.0.tgz#da04ece1967b6c2fe8f10c3473dabf3825795ef7"
@@ -5725,17 +5648,6 @@
"@typescript-eslint/visitor-keys" "8.58.0"
debug "^4.4.3"
"@typescript-eslint/parser@^7.1.1":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.18.0.tgz#83928d0f1b7f4afa974098c64b5ce6f9051f96a0"
integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==
dependencies:
"@typescript-eslint/scope-manager" "7.18.0"
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/typescript-estree" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
debug "^4.3.4"
"@typescript-eslint/project-service@8.58.0":
version "8.58.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.58.0.tgz#66ceda0aabf7427aec3e2713fa43eb278dead2aa"
@@ -5745,22 +5657,6 @@
"@typescript-eslint/types" "^8.58.0"
debug "^4.4.3"
"@typescript-eslint/scope-manager@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c"
integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==
dependencies:
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/visitor-keys" "5.62.0"
"@typescript-eslint/scope-manager@7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz#c928e7a9fc2c0b3ed92ab3112c614d6bd9951c83"
integrity sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==
dependencies:
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
"@typescript-eslint/scope-manager@8.58.0":
version "8.58.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz#e304142775e49a1b7ac3c8bf2536714447c72cab"
@@ -5774,16 +5670,6 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz#c5a8edb21f31e0fdee565724e1b984171c559482"
integrity sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==
"@typescript-eslint/type-utils@7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz#2165ffaee00b1fbbdd2d40aa85232dab6998f53b"
integrity sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==
dependencies:
"@typescript-eslint/typescript-estree" "7.18.0"
"@typescript-eslint/utils" "7.18.0"
debug "^4.3.4"
ts-api-utils "^1.3.0"
"@typescript-eslint/type-utils@8.58.0":
version "8.58.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz#ce0e72cd967ffbbe8de322db6089bd4374be352f"
@@ -5795,48 +5681,11 @@
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
"@typescript-eslint/types@7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.18.0.tgz#b90a57ccdea71797ffffa0321e744f379ec838c9"
integrity sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==
"@typescript-eslint/types@8.58.0", "@typescript-eslint/types@^8.56.0", "@typescript-eslint/types@^8.58.0":
version "8.58.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.58.0.tgz#e94ae7abdc1c6530e71183c1007b61fa93112a5a"
integrity sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==
"@typescript-eslint/typescript-estree@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==
dependencies:
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/visitor-keys" "5.62.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz#b5868d486c51ce8f312309ba79bdb9f331b37931"
integrity sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==
dependencies:
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
minimatch "^9.0.4"
semver "^7.6.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/typescript-estree@8.58.0":
version "8.58.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz#ed233faa8e2f2a2e1357c3e7d553d6465a0ee59a"
@@ -5852,17 +5701,7 @@
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.18.0.tgz#bca01cde77f95fc6a8d5b0dbcbfb3d6ca4be451f"
integrity sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@typescript-eslint/scope-manager" "7.18.0"
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/typescript-estree" "7.18.0"
"@typescript-eslint/utils@8.58.0", "@typescript-eslint/utils@^8.0.0":
"@typescript-eslint/utils@8.58.0", "@typescript-eslint/utils@^8.57.2":
version "8.58.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.58.0.tgz#21a74a7963b0d288b719a4121c7dd555adaab3c3"
integrity sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==
@@ -5872,36 +5711,6 @@
"@typescript-eslint/types" "8.58.0"
"@typescript-eslint/typescript-estree" "8.58.0"
"@typescript-eslint/utils@^5.10.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@types/json-schema" "^7.0.9"
"@types/semver" "^7.3.12"
"@typescript-eslint/scope-manager" "5.62.0"
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/typescript-estree" "5.62.0"
eslint-scope "^5.1.1"
semver "^7.3.7"
"@typescript-eslint/visitor-keys@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e"
integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==
dependencies:
"@typescript-eslint/types" "5.62.0"
eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz#0564629b6124d67607378d0f0332a0495b25e7d7"
integrity sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==
dependencies:
"@typescript-eslint/types" "7.18.0"
eslint-visitor-keys "^3.4.3"
"@typescript-eslint/visitor-keys@8.58.0":
version "8.58.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz#2abd55a4be70fd55967aceaba4330b9ba9f45189"
@@ -6486,11 +6295,6 @@ array-union@^1.0.1:
dependencies:
array-uniq "^1.0.1"
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
array-union@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-3.0.1.tgz#da52630d327f8b88cfbfb57728e2af5cd9b6b975"
@@ -8739,11 +8543,6 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
eslint-config-prettier@^8.5.0:
version "8.10.2"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz#0642e53625ebc62c31c24726b0f050df6bd97a2e"
integrity sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==
eslint-import-context@^0.1.8, eslint-import-context@^0.1.9:
version "0.1.9"
resolved "https://registry.yarnpkg.com/eslint-import-context/-/eslint-import-context-0.1.9.tgz#967b0b2f0a90ef4b689125e088f790f0b7756dbe"
@@ -8769,23 +8568,7 @@ eslint-import-resolver-typescript@^4.4.4:
version "0.0.0"
uid ""
eslint-plugin-eslint-comments@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz#9e1cd7b4413526abb313933071d7aba05ca12ffa"
integrity sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==
dependencies:
escape-string-regexp "^1.0.5"
ignore "^5.0.5"
eslint-plugin-ft-flow@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-ft-flow/-/eslint-plugin-ft-flow-2.0.3.tgz#3b3c113c41902bcbacf0e22b536debcfc3c819e8"
integrity sha512-Vbsd/b+LYA99jUbsL6viEUWShFaYQt2YQs3QN3f+aeszOhh2sgdcU0mjzDyD4yyBvMc8qy2uwvBBWfMzEX06tg==
dependencies:
lodash "^4.17.21"
string-natural-compare "^3.0.1"
eslint-plugin-import-x@^4.16.1:
eslint-plugin-import-x@^4.16.2:
version "4.16.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.2.tgz#95d1f798795566712c87897317ef8433d101db29"
integrity sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==
@@ -8801,20 +8584,13 @@ eslint-plugin-import-x@^4.16.1:
stable-hash-x "^0.2.0"
unrs-resolver "^1.9.2"
eslint-plugin-jest@^27.9.0:
version "27.9.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz#7c98a33605e1d8b8442ace092b60e9919730000b"
integrity sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==
eslint-plugin-lingui@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-lingui/-/eslint-plugin-lingui-0.12.0.tgz#fca175ad3ed40538483bfcd50c091851fefc3e8b"
integrity sha512-2+9P3thudGIBI10sDWYUIrGs3HIP09gv0XF98RDzZs34GAsAsGTUoSgrttKS1knAU5dwbrFKhNKu2LMrjRECag==
dependencies:
"@typescript-eslint/utils" "^5.10.0"
eslint-plugin-lingui@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-lingui/-/eslint-plugin-lingui-0.11.0.tgz#e33a4fe83698bb4cdfbfa816391fb79b68e6c026"
integrity sha512-O2Ixoapt5fa4VKZJgXhVwb6BHnzByIUDNMfZOhHWGMYk40GfGCho4MUfspLVrHAFLimgBPKXtCcJ8GC4YNZmfg==
dependencies:
"@typescript-eslint/utils" "^8.0.0"
micromatch "^4.0.0"
"@typescript-eslint/utils" "^8.57.2"
micromatch "^4.0.8"
eslint-plugin-react-compiler@^19.1.0-rc.2:
version "19.1.0-rc.2"
@@ -8828,11 +8604,6 @@ eslint-plugin-react-compiler@^19.1.0-rc.2:
zod "^3.22.4"
zod-validation-error "^3.0.3"
eslint-plugin-react-hooks@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3"
integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==
eslint-plugin-react-hooks@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169"
@@ -8858,13 +8629,6 @@ eslint-plugin-react-native-globals@^0.1.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz#ee1348bc2ceb912303ce6bdbd22e2f045ea86ea2"
integrity sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==
eslint-plugin-react-native@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-native/-/eslint-plugin-react-native-4.1.0.tgz#5343acd3b2246bc1b857ac38be708f070d18809f"
integrity sha512-QLo7rzTBOl43FvVqDdq5Ql9IoElIuTdjrz9SKAXCvULvBoRZ44JGSkx9z4999ZusCsb4rK3gjS8gOGyeYqZv2Q==
dependencies:
eslint-plugin-react-native-globals "^0.1.1"
eslint-plugin-react-native@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-native/-/eslint-plugin-react-native-5.0.0.tgz#2ee990ba4967c557183b31121578547fb5c02d5d"
@@ -8872,7 +8636,7 @@ eslint-plugin-react-native@^5.0.0:
dependencies:
eslint-plugin-react-native-globals "^0.1.1"
eslint-plugin-react@^7.30.1, eslint-plugin-react@^7.37.5:
eslint-plugin-react@^7.37.5:
version "7.37.5"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065"
integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==
@@ -8901,7 +8665,7 @@ eslint-plugin-simple-import-sort@^12.1.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz#e64bfdaf91c5b98a298619aa634a9f7aa43b709e"
integrity sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==
eslint-scope@5.1.1, eslint-scope@^5.1.1:
eslint-scope@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@@ -8917,12 +8681,7 @@ eslint-scope@^8.4.0:
esrecurse "^4.3.0"
estraverse "^5.2.0"
eslint-visitor-keys@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3:
eslint-visitor-keys@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
@@ -9332,6 +9091,11 @@ expo-notifications@~0.32.16:
expo-application "~7.0.8"
expo-constants "~18.0.13"
expo-paste-input@^0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/expo-paste-input/-/expo-paste-input-0.1.12.tgz#0295eb26caf738fc39019f4ecae48c22b126b67e"
integrity sha512-iaOCgygmCYVbSj+gJOxr8P28y8Tf0X4UHtVVXRjEsARKf5gQUoVaRX63uzB+8h+g6vN710fSmu5vdxNP28bw8g==
expo-privacy-sensitive@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/expo-privacy-sensitive/-/expo-privacy-sensitive-0.1.0.tgz#2177d7a3cb8ed352df94c5806d012dfb7b48bc84"
@@ -9512,17 +9276,6 @@ fast-glob@^3.2.7:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^3.2.9:
version "3.3.3"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.8"
fast-glob@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
@@ -10080,18 +9833,6 @@ globalthis@^1.0.4:
define-properties "^1.2.1"
gopd "^1.0.1"
globby@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
dependencies:
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.2.9"
ignore "^5.2.0"
merge2 "^1.4.1"
slash "^3.0.0"
globby@^12.0.2:
version "12.2.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-12.2.0.tgz#2ab8046b4fba4ff6eede835b29f678f90e3d3c22"
@@ -10132,11 +9873,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0,
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
graphemer@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
gzip-size@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
@@ -10487,11 +10223,6 @@ ieee754@^1.1.13:
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ignore@^5.0.5:
version "5.3.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
ignore@^5.1.9, ignore@^5.2.0:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
@@ -12501,7 +12232,7 @@ micromatch@4.0.5, micromatch@^4.0.2, micromatch@^4.0.4:
braces "^3.0.2"
picomatch "^2.3.1"
micromatch@^4.0.0, micromatch@^4.0.7, micromatch@^4.0.8:
micromatch@^4.0.7, micromatch@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
@@ -14130,6 +13861,11 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.3.tgz#5e3ca90e682fed1d73a7cb50c2c7402b3e85618d"
integrity sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g==
react-hotkeys-hook@5.2.4:
version "5.2.4"
resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-5.2.4.tgz#45ad54d78823b2a929963d482aff98efad0530f2"
integrity sha512-BgKg+A1+TawkYluh5Bo4cTmcgMN5L29uhJbDUQdHwPX+qgXRjIPYU5kIDHyxnAwCkCBiu9V5OpB2mpyeluVF2A==
react-image-crop@^11.0.7:
version "11.0.7"
resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-11.0.7.tgz#25f3d37ccbb65a05d19d23b4740a5912835c741e"
@@ -14932,16 +14668,16 @@ selfsigned@^2.1.1:
dependencies:
node-forge "^1"
semver@7.6.3, semver@^7.1.3:
version "7.6.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.1.3:
version "7.6.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@~7.5.4:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
@@ -14949,11 +14685,6 @@ semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@~7.5.4:
dependencies:
lru-cache "^6.0.0"
semver@^7.3.7:
version "7.7.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
semver@^7.6.0:
version "7.6.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
@@ -15507,11 +15238,6 @@ string-length@^5.0.1:
char-regex "^2.0.0"
strip-ansi "^7.0.1"
string-natural-compare@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -16022,11 +15748,6 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
ts-api-utils@^1.3.0:
version "1.4.3"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064"
integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==
ts-api-utils@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1"
@@ -16042,11 +15763,6 @@ ts-plugin-sort-import-suggestions@^1.0.4:
resolved "https://registry.yarnpkg.com/ts-plugin-sort-import-suggestions/-/ts-plugin-sort-import-suggestions-1.0.4.tgz#d1ed6c235feb8c8bb8b34c625ea75b46e3e62925"
integrity sha512-85n5lm2OQQ+b7aRNK9omU1gmjMNXRsgeLwojm5u4OSY5sVBkAHTcgMQPEeHMNlyyfFW0uXnwgqAU0pNfhD96Bw==
tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
@@ -16057,13 +15773,6 @@ tslib@^2.8.0, tslib@^2.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
dependencies:
tslib "^1.8.1"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@@ -16188,7 +15897,7 @@ typed-array-length@^1.0.7:
possible-typed-array-names "^1.0.0"
reflect.getprototypeof "^1.0.6"
typescript-eslint@^8.57.2:
typescript-eslint@^8.58.0:
version "8.58.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.58.0.tgz#5758b1b68ae7ec05d756b98c63a1f6953a01172b"
integrity sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==