Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 493a4928f9 | |||
| 20d4266e2f | |||
| c9e429760d | |||
| be33c626b0 | |||
| 1a160df396 | |||
| de97c07c63 | |||
| c339dc5b05 | |||
| 3ed237b629 | |||
| 21c288272d | |||
| c4fffba11b | |||
| e4e5a589b1 | |||
| b1b6affb71 | |||
| 1ac307bc42 | |||
| 80c0125d6b | |||
| c959dc351f | |||
| b806f3e85d | |||
| c5e60ba006 | |||
| 55222c5b0e | |||
| ca8ca903b2 | |||
| da6bcc54cf | |||
| adbc27059a | |||
| ce69e052b7 | |||
| f6aecf5c28 | |||
| 41228dce19 | |||
| 356dad1932 | |||
| e2a7965e43 | |||
| 5655241bef | |||
| add7a91d54 | |||
| 8a790cdd7f | |||
| 10dd0f76eb | |||
| 6e20893a0c | |||
| 2276cb0e75 | |||
| 2808f8b73d | |||
| ffc63dc85f | |||
| 4b32b0a71b | |||
| f8cdd6b9ae | |||
| 10e241e7eb | |||
| 88166926fa |
@@ -2,7 +2,6 @@
|
||||
|
||||
BITDRIFT_API_KEY=
|
||||
SENTRY_AUTH_TOKEN=
|
||||
EXPO_PUBLIC_ENV=development
|
||||
EXPO_PUBLIC_LOG_LEVEL=debug
|
||||
EXPO_PUBLIC_LOG_DEBUG=
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
|
||||
|
||||
@@ -54,4 +54,4 @@ jobs:
|
||||
run: yarn intl:build
|
||||
- name: Run tests
|
||||
run: |
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test --forceExit
|
||||
NODE_ENV=test yarn test --forceExit
|
||||
|
||||
@@ -20,7 +20,7 @@ build-web-embed: ## Compile web embed bundle, copy to bskyweb/embedr* directorie
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run all tests
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test
|
||||
NODE_ENV=test yarn test
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run style checks and verify syntax
|
||||
|
||||
+1
-1
@@ -15,9 +15,9 @@ module.exports = function (config) {
|
||||
*/
|
||||
const PLATFORM = process.env.EAS_BUILD_PLATFORM
|
||||
|
||||
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
|
||||
const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production'
|
||||
const IS_DEV = !IS_TESTFLIGHT || !IS_PRODUCTION
|
||||
|
||||
const ASSOCIATED_DOMAINS = [
|
||||
'applinks:bsky.app',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z"/></svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -5,9 +5,8 @@ import {LogBox} from 'react-native'
|
||||
import {registerRootComponent} from 'expo'
|
||||
|
||||
import App from '#/App'
|
||||
import {IS_TEST} from '#/env'
|
||||
|
||||
if (IS_TEST) {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
LogBox.ignoreAllLogs() // suppress all logs in tests
|
||||
} else {
|
||||
LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine
|
||||
|
||||
+11
-11
@@ -54,7 +54,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.13.18",
|
||||
"@atproto/api": "^0.13.20",
|
||||
"@bitdrift/react-native": "0.4.0",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -63,6 +63,7 @@
|
||||
"@expo/webpack-config": "^19.0.0",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@floating-ui/react-dom": "^2.0.8",
|
||||
"@formatjs/intl-datetimeformat": "^6.17.1",
|
||||
"@formatjs/intl-locale": "^4.2.8",
|
||||
"@formatjs/intl-numberformat": "^8.15.1",
|
||||
"@formatjs/intl-pluralrules": "^5.4.1",
|
||||
@@ -75,7 +76,7 @@
|
||||
"@lingui/react": "^4.14.1",
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.4.1",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
||||
"@radix-ui/react-dismissable-layer": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "2.0.1",
|
||||
"@radix-ui/react-focus-guards": "^1.1.1",
|
||||
@@ -119,7 +120,7 @@
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "~52.0.17",
|
||||
"expo": "~52.0.19",
|
||||
"expo-application": "^6.0.1",
|
||||
"expo-blur": "^14.0.1",
|
||||
"expo-build-properties": "^0.13.1",
|
||||
@@ -127,7 +128,7 @@
|
||||
"expo-clipboard": "^7.0.0",
|
||||
"expo-dev-client": "^5.0.4",
|
||||
"expo-device": "~7.0.1",
|
||||
"expo-file-system": "^18.0.4",
|
||||
"expo-file-system": "^18.0.6",
|
||||
"expo-font": "~13.0.1",
|
||||
"expo-haptics": "^14.0.0",
|
||||
"expo-image": "~2.0.3",
|
||||
@@ -138,13 +139,13 @@
|
||||
"expo-localization": "~16.0.0",
|
||||
"expo-media-library": "~17.0.3",
|
||||
"expo-navigation-bar": "~4.0.4",
|
||||
"expo-notifications": "~0.29.10",
|
||||
"expo-notifications": "~0.29.11",
|
||||
"expo-sharing": "^13.0.0",
|
||||
"expo-splash-screen": "~0.29.16",
|
||||
"expo-splash-screen": "~0.29.18",
|
||||
"expo-status-bar": "~2.0.0",
|
||||
"expo-system-ui": "^4.0.4",
|
||||
"expo-task-manager": "~12.0.3",
|
||||
"expo-updates": "~0.26.9",
|
||||
"expo-updates": "~0.26.10",
|
||||
"expo-web-browser": "~14.0.1",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
@@ -206,13 +207,12 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.64",
|
||||
"@atproto/dev-env": "^0.3.67",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@did-plc/server": "^0.0.1",
|
||||
"@expo/config-plugins": "9.0.10",
|
||||
"@expo/prebuild-config": "8.0.22",
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
@@ -267,8 +267,8 @@
|
||||
"@react-native/normalize-colors": "0.76.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@types/react": "^18",
|
||||
"**/expo-constants": "16.0.1",
|
||||
"**/expo-device": "6.0.2",
|
||||
"**/expo-constants": "17.0.3",
|
||||
"**/expo-device": "7.0.1",
|
||||
"**/zod": "3.23.8"
|
||||
},
|
||||
"jest": {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
diff --git a/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts b/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts
|
||||
new file mode 100644
|
||||
index 0000000..babbb3b
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts
|
||||
@@ -0,0 +1,60 @@
|
||||
+import { useReleasingSharedObject } from 'expo-modules-core';
|
||||
+import { SharedRef } from 'expo-modules-core/types';
|
||||
+
|
||||
+import { Action, ImageResult, SaveFormat, SaveOptions } from './ImageManipulator.types';
|
||||
+import { ImageManipulatorContext } from './ImageManipulatorContext';
|
||||
+import ExpoImageManipulator from './NativeImageManipulatorModule';
|
||||
+import { validateArguments } from './validators';
|
||||
+
|
||||
+// @needsAudit
|
||||
+/**
|
||||
+ * Manipulate the image provided via `uri`. Available modifications are rotating, flipping (mirroring),
|
||||
+ * resizing and cropping. Each invocation results in a new file. With one invocation you can provide
|
||||
+ * a set of actions to perform over the image. Overwriting the source file would not have an effect
|
||||
+ * in displaying the result as images are cached.
|
||||
+ * @param uri URI of the file to manipulate. Should be on the local file system or a base64 data URI.
|
||||
+ * @param actions An array of objects representing manipulation options. Each object should have
|
||||
+ * __only one__ of the keys that corresponds to specific transformation.
|
||||
+ * @param saveOptions A map defining how modified image should be saved.
|
||||
+ * @return Promise which fulfils with [`ImageResult`](#imageresult) object.
|
||||
+ * @deprecated It has been replaced by the new, contextual and object-oriented API.
|
||||
+ * Use [`ImageManipulator.manipulate`](#manipulateuri) or [`useImageManipulator`](#useimagemanipulatoruri) instead.
|
||||
+ */
|
||||
+export async function manipulateAsync(
|
||||
+ uri: string,
|
||||
+ actions: Action[] = [],
|
||||
+ saveOptions: SaveOptions = {}
|
||||
+): Promise<ImageResult> {
|
||||
+ validateArguments(uri, actions, saveOptions);
|
||||
+
|
||||
+ const { format = SaveFormat.JPEG, ...rest } = saveOptions;
|
||||
+ const context = ExpoImageManipulator.manipulate(uri);
|
||||
+
|
||||
+ for (const action of actions) {
|
||||
+ if ('resize' in action) {
|
||||
+ context.resize(action.resize);
|
||||
+ } else if ('rotate' in action) {
|
||||
+ context.rotate(action.rotate);
|
||||
+ } else if ('flip' in action) {
|
||||
+ context.flip(action.flip);
|
||||
+ } else if ('crop' in action) {
|
||||
+ context.crop(action.crop);
|
||||
+ } else if ('extent' in action && context.extent) {
|
||||
+ context.extent(action.extent);
|
||||
+ }
|
||||
+ }
|
||||
+ const image = await context.renderAsync(saveOptions.compress);
|
||||
+ const result = await image.saveAsync({ format, ...rest });
|
||||
+
|
||||
+ // These shared objects will not be used anymore, so free up some memory.
|
||||
+ context.release();
|
||||
+ image.release();
|
||||
+
|
||||
+ return result;
|
||||
+}
|
||||
+
|
||||
+export function useImageManipulator(source: string | SharedRef<'image'>): ImageManipulatorContext {
|
||||
+ return useReleasingSharedObject(() => ExpoImageManipulator.manipulate(source), [source]);
|
||||
+}
|
||||
+
|
||||
+export { ExpoImageManipulator as ImageManipulator };
|
||||
diff --git a/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts b/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts
|
||||
index 120d8d3..f8aa49c 100644
|
||||
--- a/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts
|
||||
+++ b/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts
|
||||
@@ -52,7 +52,7 @@ export declare class ImageManipulatorContext extends SharedObject {
|
||||
/**
|
||||
* Awaits for all manipulation tasks to finish and resolves with a reference to the resulted native image.
|
||||
*/
|
||||
- renderAsync(): Promise<ImageRef>;
|
||||
+ renderAsync(compress?: number): Promise<ImageRef>;
|
||||
}
|
||||
|
||||
export default ExpoImageManipulator.Context as typeof ImageManipulatorContext;
|
||||
diff --git a/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts b/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts
|
||||
index 428848c..363a57a 100644
|
||||
--- a/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts
|
||||
+++ b/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts
|
||||
@@ -41,7 +41,7 @@ export default class ImageManipulatorContext extends SharedObject {
|
||||
return this;
|
||||
}
|
||||
|
||||
- async renderAsync(): Promise<ImageManipulatorImageRef> {
|
||||
+ async renderAsync(compress?: number): Promise<ImageManipulatorImageRef> {
|
||||
const canvas = await this.currentTask;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
@@ -49,7 +49,7 @@ export default class ImageManipulatorContext extends SharedObject {
|
||||
const url = blob ? URL.createObjectURL(blob) : canvas.toDataURL();
|
||||
|
||||
resolve(new ImageManipulatorImageRef(url, canvas.width, canvas.height));
|
||||
- });
|
||||
+ }, typeof compress === 'number' ? 'image/jpeg' : undefined, compress);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ index 40aaf9c..1c60164 100644
|
||||
{
|
||||
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
|
||||
index e9ce48c..ccd9ad6 100644
|
||||
index e9ce48c..84a6fca 100644
|
||||
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
|
||||
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
|
||||
@@ -159,26 +159,8 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
|
||||
@@ -329,3 +329,38 @@ index e9ce48c..ccd9ad6 100644
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1055,6 +1082,22 @@ -(type)getter \
|
||||
RCT_SET_AND_PRESERVE_OFFSET(setShowsVerticalScrollIndicator, showsVerticalScrollIndicator, BOOL)
|
||||
RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat);
|
||||
|
||||
+- (void)setScrollIndicatorInsets:(UIEdgeInsets)value
|
||||
+{
|
||||
+ [_scrollView setScrollIndicatorInsets:value];
|
||||
+}
|
||||
+
|
||||
+- (UIEdgeInsets)scrollIndicatorInsets
|
||||
+{
|
||||
+ UIEdgeInsets verticalScrollIndicatorInsets = [_scrollView verticalScrollIndicatorInsets];
|
||||
+ UIEdgeInsets horizontalScrollIndicatorInsets = [_scrollView horizontalScrollIndicatorInsets];
|
||||
+ return UIEdgeInsetsMake(
|
||||
+ verticalScrollIndicatorInsets.top,
|
||||
+ horizontalScrollIndicatorInsets.left,
|
||||
+ verticalScrollIndicatorInsets.bottom,
|
||||
+ horizontalScrollIndicatorInsets.right);
|
||||
+}
|
||||
+
|
||||
- (void)setAutomaticallyAdjustsScrollIndicatorInsets:(BOOL)automaticallyAdjusts API_AVAILABLE(ios(13.0))
|
||||
{
|
||||
// `automaticallyAdjustsScrollIndicatorInsets` is available since iOS 13.
|
||||
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
index cd1e7eb..c1d0172 100644
|
||||
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
@@ -83,6 +83,7 @@ - (UIView *)view
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval)
|
||||
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
||||
+RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(verticalScrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
|
||||
|
||||
+22
-21
@@ -17,11 +17,7 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {
|
||||
initialize,
|
||||
Provider as StatsigProvider,
|
||||
tryFetchGates,
|
||||
} from '#/lib/statsig/statsig'
|
||||
import {Provider as StatsigProvider, tryFetchGates} from '#/lib/statsig/statsig'
|
||||
import {s} from '#/lib/styles'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
@@ -36,6 +32,7 @@ import {
|
||||
ensureGeolocationResolved,
|
||||
Provider as GeolocationProvider,
|
||||
} from '#/state/geolocation'
|
||||
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -54,6 +51,7 @@ import {
|
||||
import {readLastActiveAccount} from '#/state/session/util'
|
||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||
import {Provider as ComposerProvider} from '#/state/shell/composer'
|
||||
import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar'
|
||||
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
@@ -100,7 +98,6 @@ function InnerApp() {
|
||||
if (account) {
|
||||
await resumeSession(account)
|
||||
} else {
|
||||
await initialize()
|
||||
await tryFetchGates(undefined, 'prefer-fresh-gates')
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -141,20 +138,22 @@ function InnerApp() {
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
@@ -209,7 +208,9 @@ function App() {
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<IntentDialogProvider>
|
||||
<InnerApp />
|
||||
<LightStatusBarProvider>
|
||||
<InnerApp />
|
||||
</LightStatusBarProvider>
|
||||
</IntentDialogProvider>
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
|
||||
+19
-13
@@ -22,6 +22,7 @@ import {
|
||||
ensureGeolocationResolved,
|
||||
Provider as GeolocationProvider,
|
||||
} from '#/state/geolocation'
|
||||
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
import {readLastActiveAccount} from '#/state/session/util'
|
||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||
import {Provider as ComposerProvider} from '#/state/shell/composer'
|
||||
import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar'
|
||||
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
@@ -119,18 +121,20 @@ function InnerApp() {
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
@@ -181,7 +185,9 @@ function App() {
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<IntentDialogProvider>
|
||||
<InnerApp />
|
||||
<LightStatusBarProvider>
|
||||
<InnerApp />
|
||||
</LightStatusBarProvider>
|
||||
</IntentDialogProvider>
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
|
||||
+1
-1
@@ -55,7 +55,6 @@ import {NotificationsScreen} from '#/view/screens/Notifications'
|
||||
import {PostThreadScreen} from '#/view/screens/PostThread'
|
||||
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
|
||||
import {ProfileScreen} from '#/view/screens/Profile'
|
||||
import {ProfileFeedScreen} from '#/view/screens/ProfileFeed'
|
||||
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
|
||||
import {ProfileListScreen} from '#/view/screens/ProfileList'
|
||||
import {SavedFeeds} from '#/view/screens/SavedFeeds'
|
||||
@@ -75,6 +74,7 @@ import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
|
||||
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
|
||||
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
|
||||
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||
import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'
|
||||
import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
|
||||
import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
|
||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||
|
||||
@@ -60,6 +60,7 @@ export function createThemes({
|
||||
dim: Theme
|
||||
} {
|
||||
const color = {
|
||||
like: '#ec4899',
|
||||
trueBlack: '#000000',
|
||||
|
||||
gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`,
|
||||
@@ -124,6 +125,7 @@ export function createThemes({
|
||||
const lightPalette = {
|
||||
white: color.gray_0,
|
||||
black: color.gray_1000,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: color.gray_25,
|
||||
contrast_50: color.gray_50,
|
||||
@@ -185,6 +187,7 @@ export function createThemes({
|
||||
const darkPalette: Palette = {
|
||||
white: color.gray_25,
|
||||
black: color.trueBlack,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: color.gray_975,
|
||||
contrast_50: color.gray_950,
|
||||
@@ -246,6 +249,7 @@ export function createThemes({
|
||||
const dimPalette: Palette = {
|
||||
...darkPalette,
|
||||
black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`,
|
||||
contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`,
|
||||
|
||||
@@ -12,6 +12,7 @@ export type ThemeName = 'light' | 'dim' | 'dark'
|
||||
export type Palette = {
|
||||
white: string
|
||||
black: string
|
||||
like: string
|
||||
|
||||
contrast_25: string
|
||||
contrast_50: string
|
||||
|
||||
@@ -110,9 +110,7 @@ export function renderChildrenWithEmoji(
|
||||
return child.split(EMOJI).map((stringPart, index) => [
|
||||
stringPart,
|
||||
emojis[index] ? (
|
||||
<UITextView
|
||||
{...props}
|
||||
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
|
||||
<UITextView {...props} style={[props?.style, {fontFamily: 'System'}]}>
|
||||
{emojis[index]}
|
||||
</UITextView>
|
||||
) : null,
|
||||
|
||||
@@ -6,7 +6,6 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
@@ -16,7 +15,6 @@ import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {SeenPost} from '#/state/userActionHistory'
|
||||
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
||||
@@ -506,23 +504,9 @@ export function SuggestedFeeds() {
|
||||
|
||||
export function ProgressGuide() {
|
||||
const t = useTheme()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const guide = useProgressGuide('like-10-and-follow-7')
|
||||
|
||||
if (isDesktop) {
|
||||
return null
|
||||
}
|
||||
|
||||
return guide ? (
|
||||
<View
|
||||
style={[
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
a.px_lg,
|
||||
a.py_lg,
|
||||
a.pb_lg,
|
||||
]}>
|
||||
return (
|
||||
<View style={[t.atoms.border_contrast_low, a.px_lg, a.py_lg, a.pb_lg]}>
|
||||
<ProgressGuideList />
|
||||
</View>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -175,7 +175,8 @@ export function TitleText({
|
||||
gtMobile && a.text_xl,
|
||||
style,
|
||||
]}
|
||||
numberOfLines={2}>
|
||||
numberOfLines={2}
|
||||
emoji>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ export * as Header from '#/components/Layout/Header'
|
||||
|
||||
export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
noInsetTop?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,6 +29,7 @@ export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||
*/
|
||||
export const Screen = React.memo(function Screen({
|
||||
style,
|
||||
noInsetTop,
|
||||
...props
|
||||
}: ScreenProps) {
|
||||
const {top} = useSafeAreaInsets()
|
||||
@@ -35,7 +37,7 @@ export const Screen = React.memo(function Screen({
|
||||
<>
|
||||
{isWeb && <WebCenterBorders />}
|
||||
<View
|
||||
style={[a.util_screen_outer, {paddingTop: top}, style]}
|
||||
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -10,3 +10,23 @@ export const Context = React.createContext<ContextType>({
|
||||
export const ItemContext = React.createContext<ItemContextType>({
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
export function useMenuContext() {
|
||||
const context = React.useContext(Context)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useMenuContext must be used within a Context.Provider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export function useMenuItemContext() {
|
||||
const context = React.useContext(ItemContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useMenuItemContext must be used within a Context.Provider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -9,7 +9,12 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Context, ItemContext} from '#/components/Menu/context'
|
||||
import {
|
||||
Context,
|
||||
ItemContext,
|
||||
useMenuContext,
|
||||
useMenuItemContext,
|
||||
} from '#/components/Menu/context'
|
||||
import {
|
||||
ContextType,
|
||||
GroupProps,
|
||||
@@ -25,10 +30,6 @@ export {
|
||||
useDialogControl as useMenuControl,
|
||||
} from '#/components/Dialog'
|
||||
|
||||
export function useMemoControlContext() {
|
||||
return React.useContext(Context)
|
||||
}
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
control,
|
||||
@@ -47,7 +48,7 @@ export function Root({
|
||||
}
|
||||
|
||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
const {control} = React.useContext(Context)
|
||||
const context = useMenuContext()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const {
|
||||
state: pressed,
|
||||
@@ -57,14 +58,14 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
|
||||
return children({
|
||||
isNative: true,
|
||||
control,
|
||||
control: context.control,
|
||||
state: {
|
||||
hovered: false,
|
||||
focused,
|
||||
pressed,
|
||||
},
|
||||
props: {
|
||||
onPress: control.open,
|
||||
onPress: context.control.open,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onPressIn,
|
||||
@@ -82,7 +83,7 @@ export function Outer({
|
||||
showCancel?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
const context = React.useContext(Context)
|
||||
const context = useMenuContext()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
@@ -105,7 +106,7 @@ export function Outer({
|
||||
|
||||
export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
const t = useTheme()
|
||||
const {control} = React.useContext(Context)
|
||||
const context = useMenuContext()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const {
|
||||
state: pressed,
|
||||
@@ -121,10 +122,9 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onPress={async e => {
|
||||
await onPress(e)
|
||||
if (!e.defaultPrevented) {
|
||||
control?.close()
|
||||
}
|
||||
context.control.close(() => {
|
||||
onPress?.(e)
|
||||
})
|
||||
}}
|
||||
onPressIn={e => {
|
||||
onPressIn()
|
||||
@@ -156,7 +156,7 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
|
||||
export function ItemText({children, style}: ItemTextProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
const {disabled} = useMenuItemContext()
|
||||
return (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
@@ -177,7 +177,7 @@ export function ItemText({children, style}: ItemTextProps) {
|
||||
|
||||
export function ItemIcon({icon: Comp}: ItemIconProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
const {disabled} = useMenuItemContext()
|
||||
return (
|
||||
<Comp
|
||||
size="lg"
|
||||
@@ -223,7 +223,7 @@ export function Group({children, style}: GroupProps) {
|
||||
|
||||
function Cancel() {
|
||||
const {_} = useLingui()
|
||||
const {control} = React.useContext(Context)
|
||||
const context = useMenuContext()
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -231,7 +231,7 @@ function Cancel() {
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onPress={() => control.close()}>
|
||||
onPress={() => context.control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
|
||||
@@ -7,7 +7,12 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import {atoms as a, flatten, useTheme, web} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Context, ItemContext} from '#/components/Menu/context'
|
||||
import {
|
||||
Context,
|
||||
ItemContext,
|
||||
useMenuContext,
|
||||
useMenuItemContext,
|
||||
} from '#/components/Menu/context'
|
||||
import {
|
||||
ContextType,
|
||||
GroupProps,
|
||||
@@ -40,10 +45,6 @@ export function useMenuControl(): Dialog.DialogControlProps {
|
||||
)
|
||||
}
|
||||
|
||||
export function useMemoControlContext() {
|
||||
return React.useContext(Context)
|
||||
}
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
control,
|
||||
@@ -110,7 +111,7 @@ const RadixTriggerPassThrough = React.forwardRef(
|
||||
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
|
||||
|
||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
const {control} = React.useContext(Context)
|
||||
const {control} = useMenuContext()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onMouseEnter,
|
||||
@@ -203,7 +204,7 @@ export function Outer({
|
||||
|
||||
export function Item({children, label, onPress, ...rest}: ItemProps) {
|
||||
const t = useTheme()
|
||||
const {control} = React.useContext(Context)
|
||||
const {control} = useMenuContext()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onMouseEnter,
|
||||
@@ -262,7 +263,7 @@ export function Item({children, label, onPress, ...rest}: ItemProps) {
|
||||
|
||||
export function ItemText({children, style}: ItemTextProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
const {disabled} = useMenuItemContext()
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
@@ -279,7 +280,7 @@ export function ItemText({children, style}: ItemTextProps) {
|
||||
|
||||
export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
const {disabled} = useMenuItemContext()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
renderChildrenWithEmoji,
|
||||
TextProps,
|
||||
} from '#/alf/typography'
|
||||
import {IS_DEV} from '#/env'
|
||||
export type {TextProps}
|
||||
|
||||
/**
|
||||
@@ -31,7 +30,7 @@ export function Text({
|
||||
flags,
|
||||
})
|
||||
|
||||
if (IS_DEV) {
|
||||
if (__DEV__) {
|
||||
if (!emoji && childHasEmoji(children)) {
|
||||
logger.warn(
|
||||
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
|
||||
|
||||
@@ -311,7 +311,7 @@ export function PostInteractionSettingsForm({
|
||||
onChange={onChangeQuotesEnabled}
|
||||
style={[a.justify_between, a.pt_xs]}>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Quote posts enabled</Trans>
|
||||
<Trans>Allow quote posts</Trans>
|
||||
</Text>
|
||||
<Toggle.Switch />
|
||||
</Toggle.Item>
|
||||
|
||||
@@ -15,7 +15,6 @@ import {useOnboardingState} from '#/state/shell'
|
||||
* NUXs
|
||||
*/
|
||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
type Context = {
|
||||
activeNux: Nux | undefined
|
||||
@@ -93,10 +92,10 @@ function Inner({
|
||||
setActiveNux(undefined)
|
||||
}, [activeNux, setActiveNux])
|
||||
|
||||
if (IS_DEV && typeof window !== 'undefined') {
|
||||
if (__DEV__ && typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
window.clearNuxDialog = (id: Nux) => {
|
||||
if (!IS_DEV || !id) return
|
||||
if (!__DEV__ || !id) return
|
||||
resetNuxs([id])
|
||||
unsnooze()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,15 @@ import {
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {android, atoms as a, TextStyleProp, useTheme, web} from '#/alf'
|
||||
import {
|
||||
android,
|
||||
applyFonts,
|
||||
atoms as a,
|
||||
TextStyleProp,
|
||||
useAlf,
|
||||
useTheme,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -148,6 +156,7 @@ export function createInput(Component: typeof TextInput) {
|
||||
...rest
|
||||
}: InputProps) {
|
||||
const t = useTheme()
|
||||
const {fonts} = useAlf()
|
||||
const ctx = React.useContext(Context)
|
||||
const withinRoot = Boolean(ctx.inputRef)
|
||||
|
||||
@@ -171,6 +180,48 @@ export function createInput(Component: typeof TextInput) {
|
||||
|
||||
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
|
||||
|
||||
const flattened = StyleSheet.flatten([
|
||||
a.relative,
|
||||
a.z_20,
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
t.atoms.text,
|
||||
a.px_xs,
|
||||
{
|
||||
// paddingVertical doesn't work w/multiline - esb
|
||||
paddingTop: 12,
|
||||
paddingBottom: 13,
|
||||
lineHeight: a.text_md.fontSize * 1.1875,
|
||||
textAlignVertical: rest.multiline ? 'top' : undefined,
|
||||
minHeight: rest.multiline ? 80 : undefined,
|
||||
minWidth: 0,
|
||||
},
|
||||
// fix for autofill styles covering border
|
||||
web({
|
||||
paddingTop: 10,
|
||||
paddingBottom: 11,
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
}),
|
||||
android({
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}),
|
||||
style,
|
||||
])
|
||||
|
||||
applyFonts(flattened, fonts.family)
|
||||
|
||||
// should always be defined on `typography`
|
||||
// @ts-ignore
|
||||
if (flattened.fontSize) {
|
||||
// @ts-ignore
|
||||
flattened.fontSize = Math.round(
|
||||
// @ts-ignore
|
||||
flattened.fontSize * fonts.scaleMultiplier,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component
|
||||
@@ -192,35 +243,7 @@ export function createInput(Component: typeof TextInput) {
|
||||
placeholderTextColor={t.palette.contrast_500}
|
||||
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
|
||||
hitSlop={HITSLOP_20}
|
||||
style={[
|
||||
a.relative,
|
||||
a.z_20,
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
t.atoms.text,
|
||||
a.px_xs,
|
||||
{
|
||||
// paddingVertical doesn't work w/multiline - esb
|
||||
paddingTop: 12,
|
||||
paddingBottom: 13,
|
||||
lineHeight: a.text_md.fontSize * 1.1875,
|
||||
textAlignVertical: rest.multiline ? 'top' : undefined,
|
||||
minHeight: rest.multiline ? 80 : undefined,
|
||||
minWidth: 0,
|
||||
},
|
||||
// fix for autofill styles covering border
|
||||
web({
|
||||
paddingTop: 10,
|
||||
paddingBottom: 11,
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
}),
|
||||
android({
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}),
|
||||
style,
|
||||
]}
|
||||
style={flattened}
|
||||
/>
|
||||
|
||||
<View
|
||||
|
||||
@@ -8,8 +8,8 @@ export function useHeaderOffset() {
|
||||
if (isDesktop || isTablet) {
|
||||
return 0
|
||||
}
|
||||
const navBarHeight = 42
|
||||
const tabBarPad = 10 + 10 + 6 // padding + arbitrary
|
||||
const navBarHeight = 52
|
||||
const tabBarPad = 10 + 10 + 3 // padding + border
|
||||
const normalLineHeight = 20 // matches tab bar
|
||||
const tabBarText = normalLineHeight * fontScale
|
||||
return navBarHeight + tabBarPad + tabBarText
|
||||
|
||||
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
|
||||
export const Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z',
|
||||
})
|
||||
|
||||
export const Pin_Filled_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z',
|
||||
})
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
export const IS_TEST = process.env.EXPO_PUBLIC_ENV === 'test'
|
||||
export const IS_DEV = __DEV__
|
||||
export const IS_PROD = !IS_DEV
|
||||
export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || ''
|
||||
export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as
|
||||
| 'debug'
|
||||
|
||||
@@ -11,7 +11,10 @@ export async function uploadBlob(
|
||||
input: string | Blob,
|
||||
encoding?: string,
|
||||
): Promise<ComAtprotoRepoUploadBlob.Response> {
|
||||
if (typeof input === 'string' && input.startsWith('data:')) {
|
||||
if (
|
||||
typeof input === 'string' &&
|
||||
(input.startsWith('data:') || input.startsWith('blob:'))
|
||||
) {
|
||||
const blob = await fetch(input).then(r => r.blob())
|
||||
return agent.uploadBlob(blob, {encoding})
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,9 +1,7 @@
|
||||
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
|
||||
|
||||
export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV
|
||||
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||
export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
|
||||
export const IS_INTERNAL = IS_DEV || IS_TESTFLIGHT
|
||||
export const IS_INTERNAL = __DEV__ || IS_TESTFLIGHT
|
||||
|
||||
// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
|
||||
// along with the other version info. Useful for debugging/reporting.
|
||||
@@ -12,9 +10,9 @@ export const BUNDLE_IDENTIFIER = process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? ''
|
||||
// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used
|
||||
// for Statsig reporting and shouldn't be used to identify a specific bundle.
|
||||
export const BUNDLE_DATE =
|
||||
IS_TESTFLIGHT || IS_DEV ? 0 : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
|
||||
IS_TESTFLIGHT || __DEV__ ? 0 : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
|
||||
|
||||
export const appVersion = `${nativeApplicationVersion}.${nativeBuildVersion}`
|
||||
export const bundleInfo = `${BUNDLE_IDENTIFIER} (${
|
||||
IS_DEV ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod'
|
||||
__DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod'
|
||||
})`
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {version} from '../../package.json'
|
||||
|
||||
export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV
|
||||
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||
export const IS_TESTFLIGHT = false
|
||||
export const IS_INTERNAL = IS_DEV
|
||||
export const IS_INTERNAL = __DEV__
|
||||
|
||||
// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
|
||||
// along with the other version info. Useful for debugging/reporting.
|
||||
@@ -12,9 +10,9 @@ export const BUNDLE_IDENTIFIER =
|
||||
|
||||
// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used
|
||||
// for Statsig reporting and shouldn't be used to identify a specific bundle.
|
||||
export const BUNDLE_DATE = IS_DEV
|
||||
export const BUNDLE_DATE = __DEV__
|
||||
? 0
|
||||
: Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
|
||||
|
||||
export const appVersion = version
|
||||
export const bundleInfo = `${BUNDLE_IDENTIFIER} (${IS_DEV ? 'dev' : 'prod'})`
|
||||
export const bundleInfo = `${BUNDLE_IDENTIFIER} (${__DEV__ ? 'dev' : 'prod'})`
|
||||
|
||||
+16
-3
@@ -1,7 +1,20 @@
|
||||
import {init} from '@bitdrift/react-native'
|
||||
import {Statsig} from 'statsig-react-native-expo'
|
||||
|
||||
import {initPromise} from './statsig/statsig'
|
||||
|
||||
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
|
||||
|
||||
if (BITDRIFT_API_KEY) {
|
||||
init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'})
|
||||
}
|
||||
initPromise.then(() => {
|
||||
let isEnabled = false
|
||||
try {
|
||||
if (Statsig.checkGate('enable_bitdrift')) {
|
||||
isEnabled = true
|
||||
}
|
||||
} catch (e) {
|
||||
// Statsig may complain about it being called too early.
|
||||
}
|
||||
if (isEnabled && BITDRIFT_API_KEY) {
|
||||
init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -23,15 +23,6 @@ export const STARTER_PACK_MAX_SIZE = 150
|
||||
// -prf
|
||||
export const JOINED_THIS_WEEK = 2880000 // estimate as of 11/26/24
|
||||
|
||||
export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
|
||||
'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at
|
||||
'did:plc:fpruhuo22xkm5o7ttr2ktxdo': true, // danabra.mov
|
||||
'did:plc:p2cp5gopk7mgjegy6wadk3ep': true, // samuel.bsky.team
|
||||
'did:plc:ragtjsm2j2vknwkz3zp4oxrd': true, // pfrazee.com
|
||||
'did:plc:vpkhqolt662uhesyj6nxm7ys': true, // why.bsky.team
|
||||
'did:plc:3jpt2mvvsumj2r7eqk4gzzjz': true, // esb.lol
|
||||
}
|
||||
|
||||
const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
|
||||
export function FEEDBACK_FORM_URL({
|
||||
email,
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
} from 'react-native-keyboard-controller'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
const KeyboardControllerRefCountContext = createContext<{
|
||||
incrementRefCount: () => void
|
||||
decrementRefCount: () => void
|
||||
@@ -57,7 +55,7 @@ function KeyboardControllerProviderInner({
|
||||
refCount.current--
|
||||
setEnabled(refCount.current > 0)
|
||||
|
||||
if (IS_DEV && refCount.current < 0) {
|
||||
if (__DEV__ && refCount.current < 0) {
|
||||
console.error('KeyboardController ref count < 0')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -239,14 +239,21 @@ export function useNotificationsHandler() {
|
||||
)
|
||||
logEvent('notifications:openApp', {})
|
||||
invalidateCachedUnreadPage()
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
|
||||
const payload = e.notification.request.trigger
|
||||
.payload as NotificationPayload
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||
if (
|
||||
payload.reason === 'mention' ||
|
||||
payload.reason === 'quote' ||
|
||||
payload.reason === 'reply'
|
||||
) {
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||
}
|
||||
logger.debug('Notifications: handleNotification', {
|
||||
content: e.notification.request.content,
|
||||
payload: e.notification.request.trigger.payload,
|
||||
})
|
||||
handleNotification(
|
||||
e.notification.request.trigger.payload as NotificationPayload,
|
||||
)
|
||||
handleNotification(payload)
|
||||
Notifications.dismissAllNotificationsAsync()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -75,7 +75,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
|
||||
}
|
||||
|
||||
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
|
||||
Notifications: {show?: 'all'}
|
||||
Notifications: undefined
|
||||
}
|
||||
|
||||
export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
|
||||
@@ -90,7 +90,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
|
||||
Home: undefined
|
||||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
Notifications: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
||||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
NotificationsTab: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
Notifications: undefined
|
||||
MyProfileTab: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
MessagesTab: undefined
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ import {Platform} from 'react-native'
|
||||
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
|
||||
import {init} from '@sentry/react-native'
|
||||
|
||||
import {BUILD_ENV, IS_DEV, IS_TESTFLIGHT} from '#/lib/app-info'
|
||||
import {IS_TESTFLIGHT} from '#/lib/app-info'
|
||||
|
||||
/**
|
||||
* Examples:
|
||||
@@ -27,14 +27,14 @@ const release = nativeApplicationVersion ?? 'dev'
|
||||
*/
|
||||
const dist = `${Platform.OS}.${nativeBuildVersion}.${
|
||||
IS_TESTFLIGHT ? 'tf' : ''
|
||||
}${IS_DEV ? 'dev' : ''}`
|
||||
}${__DEV__ ? 'dev' : ''}`
|
||||
|
||||
init({
|
||||
enabled: !__DEV__,
|
||||
autoSessionTracking: false,
|
||||
dsn: 'https://05bc3789bf994b81bd7ce20c86ccd3ae@o4505071687041024.ingest.sentry.io/4505071690514432',
|
||||
debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production
|
||||
environment: BUILD_ENV ?? 'development',
|
||||
environment: process.env.NODE_ENV,
|
||||
dist,
|
||||
release,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export type Gate =
|
||||
// Keep this alphabetic please.
|
||||
| 'debug_show_feedcontext' // DISABLED DUE TO EME
|
||||
| 'post_feed_lang_window' // DISABLED DUE TO EME
|
||||
'debug_show_feedcontext' | 'debug_subscriptions' | 'remove_show_latest_button'
|
||||
|
||||
@@ -16,6 +16,8 @@ import {Gate} from './gates'
|
||||
|
||||
const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV'
|
||||
|
||||
export const initPromise = initialize()
|
||||
|
||||
type StatsigUser = {
|
||||
userID: string | undefined
|
||||
// TODO: Remove when enough users have custom.platform:
|
||||
@@ -219,9 +221,6 @@ export async function tryFetchGates(
|
||||
// Use this for less common operations where the user would be OK with a delay.
|
||||
timeoutMs = 1500
|
||||
}
|
||||
// Note: This condition is currently false the very first render because
|
||||
// Statsig has not initialized yet. In the future, we can fix this by
|
||||
// doing the initialization ourselves instead of relying on the provider.
|
||||
if (Statsig.initializeCalled()) {
|
||||
await Promise.race([
|
||||
timeout(timeoutMs),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Don't remove -force from these because detection is VERY slow on low-end Android.
|
||||
// https://github.com/formatjs/formatjs/issues/4463#issuecomment-2176070577
|
||||
import '@formatjs/intl-locale/polyfill-force'
|
||||
import '@formatjs/intl-datetimeformat/polyfill-force'
|
||||
import '@formatjs/intl-pluralrules/polyfill-force'
|
||||
import '@formatjs/intl-numberformat/polyfill-force'
|
||||
import '@formatjs/intl-datetimeformat/locale-data/en'
|
||||
import '@formatjs/intl-pluralrules/locale-data/en'
|
||||
import '@formatjs/intl-numberformat/locale-data/en'
|
||||
|
||||
@@ -49,6 +51,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.an: {
|
||||
i18n.loadAndActivate({locale, messages: messagesAn})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/es'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/an'),
|
||||
import('@formatjs/intl-numberformat/locale-data/es'),
|
||||
])
|
||||
@@ -57,6 +60,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.ast: {
|
||||
i18n.loadAndActivate({locale, messages: messagesAst})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ast'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ast'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ast'),
|
||||
])
|
||||
@@ -65,6 +69,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.ca: {
|
||||
i18n.loadAndActivate({locale, messages: messagesCa})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ca'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ca'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ca'),
|
||||
])
|
||||
@@ -73,6 +78,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.de: {
|
||||
i18n.loadAndActivate({locale, messages: messagesDe})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/de'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/de'),
|
||||
import('@formatjs/intl-numberformat/locale-data/de'),
|
||||
])
|
||||
@@ -81,6 +87,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.en_GB: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEn_GB})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/en-GB'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/en'),
|
||||
import('@formatjs/intl-numberformat/locale-data/en-GB'),
|
||||
])
|
||||
@@ -89,6 +96,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.es: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEs})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/es'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/es'),
|
||||
import('@formatjs/intl-numberformat/locale-data/es'),
|
||||
])
|
||||
@@ -97,6 +105,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.fi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFi})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/fi'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/fi'),
|
||||
import('@formatjs/intl-numberformat/locale-data/fi'),
|
||||
])
|
||||
@@ -105,6 +114,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.fr: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFr})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/fr'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/fr'),
|
||||
import('@formatjs/intl-numberformat/locale-data/fr'),
|
||||
])
|
||||
@@ -113,6 +123,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.ga: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGa})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ga'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ga'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ga'),
|
||||
])
|
||||
@@ -121,6 +132,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.gl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGl})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/gl'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/gl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/gl'),
|
||||
])
|
||||
@@ -129,6 +141,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.hi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesHi})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/hi'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/hi'),
|
||||
import('@formatjs/intl-numberformat/locale-data/hi'),
|
||||
])
|
||||
@@ -137,6 +150,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.hu: {
|
||||
i18n.loadAndActivate({locale, messages: messagesHu})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/hu'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/hu'),
|
||||
import('@formatjs/intl-numberformat/locale-data/hu'),
|
||||
])
|
||||
@@ -145,6 +159,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.id: {
|
||||
i18n.loadAndActivate({locale, messages: messagesId})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/id'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/id'),
|
||||
import('@formatjs/intl-numberformat/locale-data/id'),
|
||||
])
|
||||
@@ -153,6 +168,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.it: {
|
||||
i18n.loadAndActivate({locale, messages: messagesIt})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/it'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/it'),
|
||||
import('@formatjs/intl-numberformat/locale-data/it'),
|
||||
])
|
||||
@@ -161,6 +177,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.ja: {
|
||||
i18n.loadAndActivate({locale, messages: messagesJa})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ja'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ja'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ja'),
|
||||
])
|
||||
@@ -169,6 +186,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.ko: {
|
||||
i18n.loadAndActivate({locale, messages: messagesKo})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ko'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ko'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ko'),
|
||||
])
|
||||
@@ -177,6 +195,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.nl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesNl})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/nl'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/nl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/nl'),
|
||||
])
|
||||
@@ -185,6 +204,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.pl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesPl})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/pl'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/pl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/pl'),
|
||||
])
|
||||
@@ -193,6 +213,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.pt_BR: {
|
||||
i18n.loadAndActivate({locale, messages: messagesPt_BR})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/pt'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/pt'),
|
||||
import('@formatjs/intl-numberformat/locale-data/pt'),
|
||||
])
|
||||
@@ -201,6 +222,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.ru: {
|
||||
i18n.loadAndActivate({locale, messages: messagesRu})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ru'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ru'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ru'),
|
||||
])
|
||||
@@ -209,6 +231,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.th: {
|
||||
i18n.loadAndActivate({locale, messages: messagesTh})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/th'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/th'),
|
||||
import('@formatjs/intl-numberformat/locale-data/th'),
|
||||
])
|
||||
@@ -217,6 +240,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.tr: {
|
||||
i18n.loadAndActivate({locale, messages: messagesTr})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/tr'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/tr'),
|
||||
import('@formatjs/intl-numberformat/locale-data/tr'),
|
||||
])
|
||||
@@ -225,6 +249,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.uk: {
|
||||
i18n.loadAndActivate({locale, messages: messagesUk})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/uk'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/uk'),
|
||||
import('@formatjs/intl-numberformat/locale-data/uk'),
|
||||
])
|
||||
@@ -233,6 +258,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.vi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesVi})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/vi'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/vi'),
|
||||
import('@formatjs/intl-numberformat/locale-data/vi'),
|
||||
])
|
||||
@@ -241,6 +267,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.zh_CN: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_CN})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/zh-Hans'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh'),
|
||||
])
|
||||
@@ -249,6 +276,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.zh_HK: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_HK})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/yue'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh'),
|
||||
])
|
||||
@@ -257,6 +285,7 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.zh_TW: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_TW})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/zh-Hant'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh'),
|
||||
])
|
||||
|
||||
@@ -3613,7 +3613,7 @@ msgstr "Escribe la tuya clau"
|
||||
#~ msgstr "Escribe lo tuyo furnidor d'aloch preferiu"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Escribe lo tuyo identificador"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8877,7 +8877,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "La tuya canal de Seguindo ye vueda! Sigue a mas usuarios pa veyer las suyas publicacions aquí."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Lo tuyo identificador completo será"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8921,5 +8921,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Lo tuyo reporte s'ha ninviau a lo servicio de moderación de Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Lo tuyo identificador"
|
||||
|
||||
@@ -3509,7 +3509,7 @@ msgstr ""
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "¡El feed siguiente ta baleru! Sigui a más usuarios pa ver qué pasó."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "L'identificador completu va ser"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "L'informe unvióse al serviciu de moderación de Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr ""
|
||||
|
||||
@@ -4268,7 +4268,7 @@ msgstr "Introdueix la teva contrasenya"
|
||||
#~ msgstr "Introdueix el teu proveïdor d'allotjament preferit"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Introdueix el teu identificador d'usuari"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -10404,7 +10404,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "El teu canal de seguint està buit! Segueix a més usuaris per a saber què està passant."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "El teu identificador complet serà"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -10458,5 +10458,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "El teu informe s'enviarà al servei de moderació de Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "El teu identificador d'usuari"
|
||||
|
||||
@@ -3742,7 +3742,7 @@ msgstr "Gib dein Passwort ein"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Gib deinen Handle ein"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -9663,7 +9663,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Dein Following-Feed ist leer! Folge mehr Benutzern, um auf dem Laufenden zu bleiben."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Dein vollständiger Handle lautet"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -9707,5 +9707,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Dein Benutzerhandle"
|
||||
|
||||
@@ -3509,7 +3509,7 @@ msgstr ""
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr ""
|
||||
|
||||
@@ -3509,7 +3509,7 @@ msgstr ""
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr ""
|
||||
|
||||
@@ -3896,7 +3896,7 @@ msgstr "Introduce tu contraseña"
|
||||
#~ msgstr "Introduce tu proveedor de alojamiento preferido"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Introduce tu nombre de usuario"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -9518,7 +9518,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "¡Tu feed de Siguiendo esta vacío! Sigue a más usuarios para ver sus posts aquí."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Tu nombre de usuario completo será"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -9562,5 +9562,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Tu reporte ha sido enviado al servicio de moderación de Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Tu nombre de usuario"
|
||||
|
||||
@@ -3970,7 +3970,7 @@ msgstr "Syötä salasanasi"
|
||||
#~ msgstr "Syötä haluamasi palveluntarjoaja"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Syötä käyttäjätunnuksesi"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -9627,7 +9627,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Seuraamiesi syöte on tyhjä! Seuraa lisää käyttäjiä nähdäksesi, mitä tapahtuu."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Käyttäjätunnuksesi tulee olemaan"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -9671,5 +9671,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Käyttäjätunnuksesi"
|
||||
|
||||
@@ -3501,7 +3501,7 @@ msgstr "Entrez votre mot de passe"
|
||||
#~ msgstr "Entrez votre hébergeur préféré"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Entrez votre pseudo"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8519,7 +8519,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Votre fil d’actu des comptes suivis est vide ! Suivez plus de comptes pour voir ce qui se passe."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Votre nom complet sera"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8563,5 +8563,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Votre rapport sera envoyé au Service de Modération de Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Votre pseudo"
|
||||
|
||||
@@ -3508,7 +3508,7 @@ msgstr "Cuir isteach do phasfhocal"
|
||||
#~ msgstr "Cuir isteach an soláthraí óstála is fearr leat"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Cuir isteach do leasainm"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8542,7 +8542,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Tá an fotha de na daoine a leanann tú folamh! Lean tuilleadh úsáideoirí le feiceáil céard atá ar siúl."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Do leasainm iomlán anseo:"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8586,5 +8586,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Seolfar do thuairisc go dtí Seirbhís Modhnóireachta Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Do leasainm"
|
||||
|
||||
@@ -3896,7 +3896,7 @@ msgstr "Introduce o teu contrasinal"
|
||||
#~ msgstr "Introduce o teu proveedor de aloxamento preferido"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Introduce o teu alcume"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -9523,7 +9523,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "A seguinte canle está baleira! Sigue a máis persoas para ver o que está a acontecer."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "O teu alcume completo será"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -9567,5 +9567,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "O teu informe enviarase ao Servizo de moderación de Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "O teu alcume"
|
||||
|
||||
@@ -3780,7 +3780,7 @@ msgstr "अपना पासवर्ड दर्ज करें"
|
||||
#~ msgstr "अपना पसंदीदा होस्टिंग प्रदाता दर्ज करें"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "अपना उपयोगकर्ता हैंडल दर्ज करें"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -9026,7 +9026,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "आपका फ़ॉलोइंग फ़ीड खाली है! क्या चल रहा है जानने के लिए और उपयोगकर्ताओं को फ़ॉलो करें।"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "आपका पूरा हैंडल होगा"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -9070,5 +9070,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "आपके शिकायत को Bluesky मॉडरेशन सेवा को भेजा जाएगा।"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "आपका उपयोगकर्ता हैंडल"
|
||||
|
||||
@@ -3465,7 +3465,7 @@ msgstr "Jelszó megadása"
|
||||
#~ msgstr "Kívánt tárhelyszolgáltató megadása"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Felhasználónév megadása"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8439,7 +8439,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "A Követett hírfolyamod üres. Kövess több felhasználót, hogy lásd, mi történik!"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "A teljes felhasználóneved:"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8482,5 +8482,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Ez a jelentés a Bluesky moderálási szolgáltatásának lesz elküldve"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Saját felhasználónév"
|
||||
|
||||
@@ -4007,7 +4007,7 @@ msgstr "Masukkan kata sandi Anda"
|
||||
#~ msgstr "Masukkan penyedia hosting pilihan Anda"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Masukkan panggilan Anda"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -9678,7 +9678,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Feed mengikuti Anda kosong! Ikuti lebih banyak pengguna untuk melihat apa yang terjadi."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Panggilan lengkap Anda akan menjadi"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -9722,5 +9722,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Laporan Anda akan dikirim ke Layanan Moderasi Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Panggilan Anda"
|
||||
|
||||
@@ -3318,7 +3318,7 @@ msgid "Input your password"
|
||||
msgstr "Inserisci la tua password"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Inserisci il tuo nome utente"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8016,7 +8016,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Il tuo feed seguente è vuoto! Segui più utenti per vedere cosa sta succedendo."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Il tuo nome utente completo sarà"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8056,5 +8056,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "La tua segnalazione verrà inviata al Servizio Moderazione di Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Il tuo nome utente"
|
||||
|
||||
@@ -3317,7 +3317,7 @@ msgid "Input your password"
|
||||
msgstr "あなたのパスワードを入力"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "あなたのユーザーハンドルを入力"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8047,7 +8047,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Followingフィードは空です!もっと多くのユーザーをフォローして、近況を確認しましょう。"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "フルハンドルは"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8087,5 +8087,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "あなたの報告はBluesky Moderation Serviceに送られます"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "あなたのユーザーハンドル"
|
||||
|
||||
@@ -3317,7 +3317,7 @@ msgid "Input your password"
|
||||
msgstr "비밀번호를 입력합니다"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "사용자 핸들을 입력합니다"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8019,7 +8019,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "팔로우 중 피드가 비어 있습니다. 더 많은 사용자를 팔로우하여 무슨 일이 일어나고 있는지 확인하세요."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "내 전체 핸들:"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8059,5 +8059,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "신고가 Bluesky Moderation Service로 보내집니다."
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "내 사용자 핸들"
|
||||
|
||||
@@ -3496,7 +3496,7 @@ msgstr "Vul je wachtwoord in"
|
||||
#~ msgstr "Vul je voorkeurshostingprovider in"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Vul je gebruikershandle in."
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8506,7 +8506,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Je volgende feed is leeg! Volg meer gebruikers om te zien wat er gebeurt."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Je volledige handle wordt"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8550,5 +8550,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Je melding wordt verzonden naar de Bluesky-moderatieservice"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Je gebruikershandle"
|
||||
|
||||
@@ -3317,7 +3317,7 @@ msgid "Input your password"
|
||||
msgstr "Wpisz hasło"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Wpisz nazwę użytkownika"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8031,7 +8031,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Twój kanał osób obserwowanych jest pusty! Zaobserwuj trochę więcej osób."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Twoją pełna nazwa będzie"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8071,5 +8071,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Zgłoszenie będzie wysłane do usługi moderacji Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Twoja nazwa użytkownika"
|
||||
|
||||
@@ -4003,7 +4003,7 @@ msgstr "Insira sua senha"
|
||||
#~ msgstr "Insira seu provedor de hospedagem"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Insira o usuário"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -9667,7 +9667,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Seu feed inicial está vazio! Siga mais usuários para acompanhar o que está acontecendo."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Seu identificador completo será"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -9711,5 +9711,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Sua denúncia será enviada para o serviço de moderação do Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Seu identificador de usuário"
|
||||
|
||||
@@ -3578,7 +3578,7 @@ msgstr "Введите ваш пароль"
|
||||
#~ msgstr "Введите желаемого хостинг-провайдера"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Введите ваш псевдоним"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8768,7 +8768,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Ваша домашняя лента пуста! Подпишитесь на больше пользователей чтобы получать больше постов."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Ваш полный псевдоним будет"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8812,5 +8812,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Ваш отчет будет отправлен в Службу Модерации Bluesky."
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Ваш псевдоним"
|
||||
|
||||
@@ -3994,7 +3994,7 @@ msgstr "ป้อนรหัสผ่านของคุณ"
|
||||
#~ msgstr "กรอกผู้ให้บริการโฮสติ้งที่คุณต้องการ"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "กรอกชื่อผู้ใช้ของคุณ"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -9665,7 +9665,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "ฟีดผู้ติดตามของคุณว่างเปล่า! ติดตามผู้ใช้เพิ่มเติมเพื่อดูความเคลื่อนไหว"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "ชื่อผู้ใช้เต็มของคุณจะเป็น"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -9709,5 +9709,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "รายงานของคุณจะถูกส่งไปยัง Bluesky Moderation Service"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "แฮนด์เดิลผู้ใช้ของคุณ"
|
||||
|
||||
@@ -4211,7 +4211,7 @@ msgstr "Şifrenizi girin"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Kullanıcı adınızı girin"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -10212,7 +10212,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Takip ettiğiniz besleme boş! Neler olduğunu görmek için daha fazla kullanıcı takip edin."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Tam kullanıcı adınız"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -10261,5 +10261,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Kullanıcı adınız"
|
||||
|
||||
@@ -4007,7 +4007,7 @@ msgstr "Введіть ваш пароль"
|
||||
#~ msgstr "Введіть бажаного хостинг-провайдера"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Введіть ваш псевдонім"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -9678,7 +9678,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Ваша домашня стрічка порожня! Підпишіться на більше користувачів щоб отримувати більше постів."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Ваш повний псевдонім буде"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -9722,5 +9722,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Ваш псевдонім"
|
||||
|
||||
@@ -3509,7 +3509,7 @@ msgstr "Nhập mật khẩu của bạn"
|
||||
#~ msgstr "Nhập nhà cung cấp lưu trữ theo lựa chọn của bạn"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "Nhập tên người dùng của bạn"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "Bảng tin theo dõi còn trống! Hãy theo dõi thêm người dùng để biết có gì đang diễn ra."
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "Tên người dùng đầy đủ của bạn sẽ là"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "Báo cáo của bạn sẽ được gởi đến dịch vụ kiểm duyệt của Bluesky"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "Tên người dùng của bạn"
|
||||
|
||||
@@ -3314,7 +3314,7 @@ msgid "Input your password"
|
||||
msgstr "输入你的密码"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "输入你的账户代码"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8004,7 +8004,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "你的“Following”动态源是空的!关注更多用户去看看他们发了什么。"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "你的完整账户代码将修改为"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8044,5 +8044,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "你的举报将发送至 Bluesky 内容审核服务"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "你的账户代码"
|
||||
|
||||
@@ -3314,7 +3314,7 @@ msgid "Input your password"
|
||||
msgstr "輸入你嘅密碼"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "輸入你嘅帳號頭銜"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8004,7 +8004,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "你嘅「Following」動態源得個吉!跟多啲用戶睇吓發生緊啲咩事。"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "你嘅完整帳號頭銜會係"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8044,5 +8044,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "你嘅上報會傳送去 Bluesky 審核服務"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "你嘅帳號頭銜"
|
||||
|
||||
@@ -3314,7 +3314,7 @@ msgid "Input your password"
|
||||
msgstr "輸入您的密碼"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:114
|
||||
msgid "Input your user handle"
|
||||
msgid "Type your desired username"
|
||||
msgstr "輸入您的帳號代碼"
|
||||
|
||||
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
|
||||
@@ -8004,7 +8004,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
|
||||
msgstr "您的「Following」動態源是空的!跟隨更多用戶來看看發生了什麼事情。"
|
||||
|
||||
#: src/screens/Signup/StepHandle.tsx:125
|
||||
msgid "Your full handle will be"
|
||||
msgid "Your full username will be"
|
||||
msgstr "您的完整帳號代碼將修改為"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
|
||||
@@ -8044,5 +8044,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
|
||||
msgstr "您的檢舉將傳送至 Bluesky 內容管理服務"
|
||||
|
||||
#: src/screens/Signup/index.tsx:142
|
||||
msgid "Your user handle"
|
||||
msgid "Choose your username"
|
||||
msgstr "您的帳號代碼"
|
||||
|
||||
@@ -17,8 +17,8 @@ logger.error(error[, metadata])
|
||||
|
||||
#### Modes
|
||||
|
||||
The "modes" referred to here are inferred from the values exported from `#/env`.
|
||||
Basically, the booleans `IS_DEV`, `IS_TEST`, and `IS_PROD`.
|
||||
The "modes" referred to here are inferred from `process.env.NODE_ENV`,
|
||||
which matches how React Native sets the `__DEV__` global.
|
||||
|
||||
#### Log Levels
|
||||
|
||||
|
||||
@@ -5,9 +5,6 @@ import {nanoid} from 'nanoid/non-secure'
|
||||
import {Logger, LogLevel, sentryTransport} from '#/logger'
|
||||
|
||||
jest.mock('#/env', () => ({
|
||||
IS_TEST: true,
|
||||
IS_DEV: false,
|
||||
IS_PROD: false,
|
||||
/*
|
||||
* Forces debug mode for tests using the default logger. Most tests create
|
||||
* their own logger instance.
|
||||
|
||||
+12
-11
@@ -173,7 +173,7 @@ export class Logger {
|
||||
protected debugContextRegexes: RegExp[] = []
|
||||
|
||||
constructor({
|
||||
enabled = !env.IS_TEST,
|
||||
enabled = process.env.NODE_ENV !== 'test',
|
||||
level = env.LOG_LEVEL as LogLevel,
|
||||
debug = env.LOG_DEBUG || '',
|
||||
}: {
|
||||
@@ -266,17 +266,18 @@ export class Logger {
|
||||
*/
|
||||
export const logger = new Logger()
|
||||
|
||||
if (!env.IS_TEST) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
logger.addTransport(createBitdriftTransport())
|
||||
}
|
||||
|
||||
if (env.IS_DEV && !env.IS_TEST) {
|
||||
logger.addTransport(consoleTransport)
|
||||
|
||||
/*
|
||||
* Comment this out to disable Sentry transport in dev
|
||||
*/
|
||||
// logger.addTransport(sentryTransport)
|
||||
} else if (env.IS_PROD) {
|
||||
logger.addTransport(sentryTransport)
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
if (__DEV__) {
|
||||
logger.addTransport(consoleTransport)
|
||||
/*
|
||||
* Comment this out to enable Sentry transport in dev
|
||||
*/
|
||||
// logger.addTransport(sentryTransport)
|
||||
} else {
|
||||
logger.addTransport(sentryTransport)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function HashtagScreen({
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.Outer noBottomBorder>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText>
|
||||
@@ -134,7 +134,7 @@ export default function HashtagScreen({
|
||||
<Pager
|
||||
onPageSelected={onPageSelected}
|
||||
renderTabBar={props => (
|
||||
<Layout.Center style={web([a.sticky, a.z_10, {top: 0}])}>
|
||||
<Layout.Center style={[a.z_10, web([a.sticky, {top: 0}])]}>
|
||||
<TabBar items={sections.map(section => section.title)} {...props} />
|
||||
</Layout.Center>
|
||||
)}
|
||||
|
||||
@@ -60,6 +60,8 @@ export const LoginForm = ({
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
|
||||
useState<boolean>(false)
|
||||
const [isAuthFactorTokenValueEmpty, setIsAuthFactorTokenValueEmpty] =
|
||||
useState<boolean>(true)
|
||||
const identifierValueRef = useRef<string>(initialHandle || '')
|
||||
const passwordValueRef = useRef<string>('')
|
||||
const authFactorTokenValueRef = useRef<string>('')
|
||||
@@ -262,6 +264,7 @@ export const LoginForm = ({
|
||||
textContentType="username"
|
||||
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
|
||||
onChangeText={v => {
|
||||
setIsAuthFactorTokenValueEmpty(v === '')
|
||||
authFactorTokenValueRef.current = v
|
||||
}}
|
||||
onSubmitEditing={onPressNext}
|
||||
@@ -269,6 +272,13 @@ export const LoginForm = ({
|
||||
accessibilityHint={_(
|
||||
msg`Input the code which has been emailed to you`,
|
||||
)}
|
||||
style={[
|
||||
{
|
||||
textTransform: isAuthFactorTokenValueEmpty
|
||||
? 'none'
|
||||
: 'uppercase',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TextField.Root>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.mt_sm]}>
|
||||
|
||||
@@ -21,7 +21,6 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import {P, Text} from '#/components/Typography'
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
const COL_WIDTH = 420
|
||||
|
||||
@@ -64,7 +63,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
a.flex_1,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
{IS_DEV && (
|
||||
{__DEV__ && (
|
||||
<View style={[a.absolute, a.p_xl, a.z_10, {right: 0, top: insets.top}]}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -10,6 +10,7 @@ import Animated, {
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {BlurView} from 'expo-blur'
|
||||
import {useIsFetching} from '@tanstack/react-query'
|
||||
|
||||
@@ -32,7 +33,7 @@ export function GrowableBanner({
|
||||
}) {
|
||||
const pagerContext = usePagerHeaderContext()
|
||||
|
||||
// pagerContext should only be present on iOS, but better safe than sorry
|
||||
// plain non-growable mode for Android/Web
|
||||
if (!pagerContext || !isIOS) {
|
||||
return (
|
||||
<View style={[a.w_full, a.h_full]}>
|
||||
@@ -60,6 +61,7 @@ function GrowableBannerInner({
|
||||
backButton?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
const isFetching = useIsProfileFetching()
|
||||
const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY})
|
||||
|
||||
@@ -104,7 +106,7 @@ function GrowableBannerInner({
|
||||
const animatedBackButtonStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], {
|
||||
translateY: interpolate(scrollY.get(), [-150, 10], [-150, 10], {
|
||||
extrapolateRight: Extrapolation.CLAMP,
|
||||
}),
|
||||
},
|
||||
@@ -128,7 +130,14 @@ function GrowableBannerInner({
|
||||
animatedProps={animatedBlurViewProps}
|
||||
/>
|
||||
</Animated.View>
|
||||
<View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
{top: topInset - (isIOS ? 15 : 0)},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
<Animated.View style={[animatedSpinnerStyle]}>
|
||||
<ActivityIndicator
|
||||
key={animateSpinner ? 'spin' : 'stop'}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React, {memo} from 'react'
|
||||
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {BACK_HITSLOP} from '#/lib/constants'
|
||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
@@ -18,11 +17,13 @@ import {useSession} from '#/state/session'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {UserBanner} from '#/view/com/util/UserBanner'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
|
||||
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
|
||||
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
|
||||
import {GrowableAvatar} from './GrowableAvatar'
|
||||
import {GrowableBanner} from './GrowableBanner'
|
||||
import {StatusBarShadow} from './StatusBarShadow'
|
||||
|
||||
interface Props {
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||
@@ -43,7 +44,8 @@ let ProfileHeaderShell = ({
|
||||
const {_} = useLingui()
|
||||
const {openLightbox} = useLightboxControls()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
|
||||
const aviRef = useHandleRef()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
@@ -100,10 +102,11 @@ let ProfileHeaderShell = ({
|
||||
<View
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}
|
||||
style={[a.relative, {height: 150}]}>
|
||||
<StatusBarShadow />
|
||||
<GrowableBanner
|
||||
backButton={
|
||||
<>
|
||||
{!isDesktop && !hideBackButton && (
|
||||
{!hideBackButton && (
|
||||
<TouchableWithoutFeedback
|
||||
testID="profileHeaderBackBtn"
|
||||
onPress={onPressBack}
|
||||
@@ -111,12 +114,17 @@ let ProfileHeaderShell = ({
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Back`)}
|
||||
accessibilityHint="">
|
||||
<View style={styles.backBtnWrapper}>
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
color="white"
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.backBtnWrapper,
|
||||
{
|
||||
top: platform({
|
||||
web: 10,
|
||||
default: topInset,
|
||||
}),
|
||||
},
|
||||
]}>
|
||||
<ArrowLeftIcon size="lg" fill="white" />
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
@@ -186,7 +194,6 @@ export {ProfileHeaderShell}
|
||||
const styles = StyleSheet.create({
|
||||
backBtnWrapper: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
width: 30,
|
||||
height: 30,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import Animated, {SharedValue, useAnimatedStyle} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)
|
||||
|
||||
export function StatusBarShadow() {
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
const pagerContext = usePagerHeaderContext()
|
||||
|
||||
if (isIOS && pagerContext) {
|
||||
const {scrollY} = pagerContext
|
||||
return <StatusBarShadowInnner scrollY={scrollY} />
|
||||
}
|
||||
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={['rgba(0,0,0,0.5)', 'rgba(0,0,0,0)']}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
{height: topInset, top: 0, left: 0, right: 0},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusBarShadowInnner({scrollY}: {scrollY: SharedValue<number>}) {
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateY: Math.min(0, scrollY.get()),
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<AnimatedLinearGradient
|
||||
colors={['rgba(0,0,0,0.5)', 'rgba(0,0,0,0)']}
|
||||
style={[
|
||||
animatedStyle,
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
{height: topInset, top: 0, left: 0, right: 0},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function StatusBarShadow() {
|
||||
return null
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
import React, {memo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import React, {memo, useState} from 'react'
|
||||
import {LayoutChangeEvent, StyleSheet, View} from 'react-native'
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyLabelerDefs,
|
||||
ModerationOpts,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {useIsFocused} from '@react-navigation/native'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
|
||||
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {useTheme} from '#/alf'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ProfileHeaderLabeler} from './ProfileHeaderLabeler'
|
||||
import {ProfileHeaderStandard} from './ProfileHeaderStandard'
|
||||
|
||||
@@ -43,20 +54,114 @@ interface Props {
|
||||
moderationOpts: ModerationOpts
|
||||
hideBackButton?: boolean
|
||||
isPlaceholderProfile?: boolean
|
||||
setMinimumHeight: (height: number) => void
|
||||
}
|
||||
|
||||
let ProfileHeader = (props: Props): React.ReactNode => {
|
||||
let ProfileHeader = ({setMinimumHeight, ...props}: Props): React.ReactNode => {
|
||||
let content
|
||||
if (props.profile.associated?.labeler) {
|
||||
if (!props.labeler) {
|
||||
return <ProfileHeaderLoading />
|
||||
content = <ProfileHeaderLoading />
|
||||
} else {
|
||||
content = <ProfileHeaderLabeler {...props} labeler={props.labeler} />
|
||||
}
|
||||
return <ProfileHeaderLabeler {...props} labeler={props.labeler} />
|
||||
} else {
|
||||
content = <ProfileHeaderStandard {...props} />
|
||||
}
|
||||
return <ProfileHeaderStandard {...props} />
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNative && (
|
||||
<MinimalHeader
|
||||
onLayout={evt => setMinimumHeight(evt.nativeEvent.layout.height)}
|
||||
profile={props.profile}
|
||||
hideBackButton={props.hideBackButton}
|
||||
/>
|
||||
)}
|
||||
{content}
|
||||
</>
|
||||
)
|
||||
}
|
||||
ProfileHeader = memo(ProfileHeader)
|
||||
export {ProfileHeader}
|
||||
|
||||
const MinimalHeader = React.memo(function MinimalHeader({
|
||||
onLayout,
|
||||
}: {
|
||||
onLayout: (e: LayoutChangeEvent) => void
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
hideBackButton?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const insets = useSafeAreaInsets()
|
||||
const ctx = usePagerHeaderContext()
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [minimalHeaderHeight, setMinimalHeaderHeight] = React.useState(0)
|
||||
const isScreenFocused = useIsFocused()
|
||||
if (!ctx) throw new Error('MinimalHeader cannot be used on web')
|
||||
const {scrollY, headerHeight} = ctx
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
// if we don't yet have the min header height in JS, hide
|
||||
if (!_WORKLET || minimalHeaderHeight === 0) {
|
||||
return {
|
||||
opacity: 0,
|
||||
}
|
||||
}
|
||||
const pastThreshold = scrollY.get() > 100
|
||||
return {
|
||||
opacity: pastThreshold
|
||||
? withTiming(1, {duration: 75})
|
||||
: withTiming(0, {duration: 75}),
|
||||
transform: [
|
||||
{
|
||||
translateY: Math.min(
|
||||
scrollY.get(),
|
||||
headerHeight - minimalHeaderHeight,
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
useAnimatedReaction(
|
||||
() => scrollY.get() > 100,
|
||||
(value, prev) => {
|
||||
if (prev !== value) {
|
||||
runOnJS(setVisible)(value)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
useSetLightStatusBar(isScreenFocused && !visible)
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents={visible ? 'auto' : 'none'}
|
||||
aria-hidden={!visible}
|
||||
accessibilityElementsHidden={!visible}
|
||||
importantForAccessibility={visible ? 'auto' : 'no-hide-descendants'}
|
||||
onLayout={evt => {
|
||||
setMinimalHeaderHeight(evt.nativeEvent.layout.height)
|
||||
onLayout(evt)
|
||||
}}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_50,
|
||||
t.atoms.bg,
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
paddingTop: insets.top,
|
||||
},
|
||||
animatedStyle,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
MinimalHeader.displayName = 'MinimalHeader'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avi: {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import React, {useCallback, useMemo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {useAnimatedRef} from 'react-native-reanimated'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
import {ComposeIcon2} from '#/lib/icons'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {s} from '#/lib/styles'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||
import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {PostFeed} from '#/view/com/posts/PostFeed'
|
||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import {Button} from '#/view/com/util/forms/Button'
|
||||
import {ListRef} from '#/view/com/util/List'
|
||||
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
||||
import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {
|
||||
ProfileFeedHeader,
|
||||
ProfileFeedHeaderSkeleton,
|
||||
} from '#/screens/Profile/components/ProfileFeedHeader'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
|
||||
export function ProfileFeedScreen(props: Props) {
|
||||
const {rkey, name: handleOrDid} = props.route.params
|
||||
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const uri = useMemo(
|
||||
() => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
|
||||
[rkey, handleOrDid],
|
||||
)
|
||||
const {error, data: resolvedUri} = useResolveUriQuery(uri)
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Layout.Screen testID="profileFeedScreenError">
|
||||
<Layout.Content>
|
||||
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
||||
<Text type="title-lg" style={[pal.text, s.mb10]}>
|
||||
<Trans>Could not load feed</Trans>
|
||||
</Text>
|
||||
<Text type="md" style={[pal.text, s.mb20]}>
|
||||
{error.toString()}
|
||||
</Text>
|
||||
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<Button
|
||||
type="default"
|
||||
accessibilityLabel={_(msg`Go back`)}
|
||||
accessibilityHint={_(msg`Returns to previous page`)}
|
||||
onPress={onPressBack}
|
||||
style={{flexShrink: 1}}>
|
||||
<Text type="button" style={pal.text}>
|
||||
<Trans>Go Back</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
return resolvedUri ? (
|
||||
<Layout.Screen>
|
||||
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
|
||||
</Layout.Screen>
|
||||
) : (
|
||||
<Layout.Screen>
|
||||
<Layout.Content>
|
||||
<ProfileFeedHeaderSkeleton />
|
||||
<PostFeedLoadingPlaceholder />
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
|
||||
|
||||
if (!preferences || !info) {
|
||||
return (
|
||||
<Layout.Content>
|
||||
<ProfileFeedHeaderSkeleton />
|
||||
<PostFeedLoadingPlaceholder />
|
||||
</Layout.Content>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ProfileFeedScreenInner
|
||||
preferences={preferences}
|
||||
feedInfo={info as FeedSourceFeedInfo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProfileFeedScreenInner({
|
||||
feedInfo,
|
||||
}: {
|
||||
preferences: UsePreferencesQueryResponse
|
||||
feedInfo: FeedSourceFeedInfo
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
const {openComposer} = useComposerControls()
|
||||
const isScreenFocused = useIsFocused()
|
||||
|
||||
useSetTitle(feedInfo?.displayName)
|
||||
|
||||
const feed = `feedgen|${feedInfo.uri}` as FeedDescriptor
|
||||
|
||||
const [hasNew, setHasNew] = React.useState(false)
|
||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const feedFeedback = useFeedFeedback(feed, hasSession)
|
||||
const scrollElRef = useAnimatedRef() as ListRef
|
||||
|
||||
const onScrollToTop = useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({
|
||||
animated: isNative,
|
||||
offset: 0, // -headerHeight,
|
||||
})
|
||||
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
|
||||
setHasNew(false)
|
||||
}, [scrollElRef, queryClient, feed, setHasNew])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isScreenFocused) {
|
||||
return
|
||||
}
|
||||
return listenSoftReset(onScrollToTop)
|
||||
}, [onScrollToTop, isScreenFocused])
|
||||
|
||||
const renderPostsEmpty = useCallback(() => {
|
||||
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
|
||||
}, [_])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProfileFeedHeader info={feedInfo} />
|
||||
|
||||
<FeedFeedbackProvider value={feedFeedback}>
|
||||
<PostFeed
|
||||
feed={feed}
|
||||
pollInterval={60e3}
|
||||
disablePoll={hasNew}
|
||||
onHasNew={setHasNew}
|
||||
scrollElRef={scrollElRef}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
renderEmptyState={renderPostsEmpty}
|
||||
/>
|
||||
</FeedFeedbackProvider>
|
||||
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onScrollToTop}
|
||||
label={_(msg`Load new posts`)}
|
||||
showIndicator={hasNew}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({})}
|
||||
icon={
|
||||
<ComposeIcon2
|
||||
strokeWidth={1.5}
|
||||
size={29}
|
||||
style={{color: 'white'}}
|
||||
/>
|
||||
}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingVertical: 7,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: 50,
|
||||
marginLeft: 6,
|
||||
},
|
||||
notFoundContainer: {
|
||||
margin: 10,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 6,
|
||||
},
|
||||
aboutSectionContainer: {
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 16,
|
||||
gap: 12,
|
||||
},
|
||||
})
|
||||
@@ -38,7 +38,7 @@ export const ProfileFollowsScreen = ({route}: Props) => {
|
||||
</Layout.Header.TitleText>
|
||||
<Layout.Header.SubtitleText>
|
||||
<Plural
|
||||
value={profile.followersCount ?? 0}
|
||||
value={profile.followsCount ?? 0}
|
||||
one="# following"
|
||||
other="# following"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,562 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {makeCustomFeedLink} from '#/lib/routes/links'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {FeedSourceFeedInfo} from '#/state/queries/feed'
|
||||
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
|
||||
import {
|
||||
useAddSavedFeedsMutation,
|
||||
usePreferencesQuery,
|
||||
useRemoveFeedMutation,
|
||||
useUpdateSavedFeedsMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {useRichText} from '#/components/hooks/useRichText'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {
|
||||
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
|
||||
Heart2_Stroke2_Corner0_Rounded as Heart,
|
||||
} from '#/components/icons/Heart2'
|
||||
import {
|
||||
Pin_Filled_Corner0_Rounded as PinFilled,
|
||||
Pin_Stroke2_Corner0_Rounded as Pin,
|
||||
} from '#/components/icons/Pin'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ProfileFeedHeaderSkeleton() {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_sm,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
height: 44,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot>
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
height: 34,
|
||||
width: 34,
|
||||
},
|
||||
]}>
|
||||
<Pin size="lg" fill={t.atoms.text_contrast_low.color} />
|
||||
</View>
|
||||
</Layout.Header.Slot>
|
||||
</Layout.Header.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
const {gtPhone, gtMobile} = useBreakpoints()
|
||||
const infoControl = Dialog.useDialogControl()
|
||||
const playHaptic = useHaptics()
|
||||
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
|
||||
const [likeUri, setLikeUri] = React.useState(info.likeUri || '')
|
||||
const isLiked = !!likeUri
|
||||
const likeCount =
|
||||
isLiked && likeUri ? (info.likeCount || 0) + 1 : info.likeCount || 0
|
||||
|
||||
const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
|
||||
useAddSavedFeedsMutation()
|
||||
const {mutateAsync: removeFeed, isPending: isRemovePending} =
|
||||
useRemoveFeedMutation()
|
||||
const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
|
||||
useUpdateSavedFeedsMutation()
|
||||
|
||||
const isFeedStateChangePending =
|
||||
isAddSavedFeedPending || isRemovePending || isUpdateFeedPending
|
||||
const savedFeedConfig = preferences?.savedFeeds?.find(
|
||||
f => f.value === info.uri,
|
||||
)
|
||||
const isSaved = Boolean(savedFeedConfig)
|
||||
const isPinned = Boolean(savedFeedConfig?.pinned)
|
||||
|
||||
const onToggleSaved = React.useCallback(async () => {
|
||||
try {
|
||||
playHaptic()
|
||||
|
||||
if (savedFeedConfig) {
|
||||
await removeFeed(savedFeedConfig)
|
||||
Toast.show(_(msg`Removed from your feeds`))
|
||||
} else {
|
||||
await addSavedFeeds([
|
||||
{
|
||||
type: 'feed',
|
||||
value: info.uri,
|
||||
pinned: false,
|
||||
},
|
||||
])
|
||||
Toast.show(_(msg`Saved to your feeds`))
|
||||
}
|
||||
} catch (err) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg`There was an issue updating your feeds, please check your internet connection and try again.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
logger.error('Failed to update feeds', {message: err})
|
||||
}
|
||||
}, [_, playHaptic, info, removeFeed, addSavedFeeds, savedFeedConfig])
|
||||
|
||||
const onTogglePinned = React.useCallback(async () => {
|
||||
try {
|
||||
playHaptic()
|
||||
|
||||
if (savedFeedConfig) {
|
||||
const pinned = !savedFeedConfig.pinned
|
||||
await updateSavedFeeds([
|
||||
{
|
||||
...savedFeedConfig,
|
||||
pinned,
|
||||
},
|
||||
])
|
||||
|
||||
if (pinned) {
|
||||
Toast.show(_(msg`Pinned ${info.displayName} to Home`))
|
||||
} else {
|
||||
Toast.show(_(msg`Unpinned ${info.displayName} from Home`))
|
||||
}
|
||||
} else {
|
||||
await addSavedFeeds([
|
||||
{
|
||||
type: 'feed',
|
||||
value: info.uri,
|
||||
pinned: true,
|
||||
},
|
||||
])
|
||||
Toast.show(_(msg`Pinned ${info.displayName} to Home`))
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
|
||||
logger.error('Failed to toggle pinned feed', {message: e})
|
||||
}
|
||||
}, [playHaptic, info, _, savedFeedConfig, updateSavedFeeds, addSavedFeeds])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout.Center
|
||||
style={[t.atoms.bg, a.z_10, web([a.sticky, a.z_10, {top: 0}])]}>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content align="left">
|
||||
<Button
|
||||
label={_(msg`Open feed info screen`)}
|
||||
style={[
|
||||
a.justify_start,
|
||||
{
|
||||
paddingVertical: isWeb ? 4 : 6,
|
||||
paddingHorizontal: 8,
|
||||
paddingRight: 12,
|
||||
},
|
||||
]}
|
||||
onPress={() => {
|
||||
playHaptic()
|
||||
infoControl.open()
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.rounded_sm,
|
||||
a.transition_transform,
|
||||
t.atoms.bg_contrast_25,
|
||||
pressed && t.atoms.bg_contrast_50,
|
||||
hovered && {
|
||||
transform: [{scaleX: 1.01}, {scaleY: 1.1}],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{info.avatar && (
|
||||
<UserAvatar size={32} type="algo" avatar={info.avatar} />
|
||||
)}
|
||||
|
||||
<View style={[a.flex_1]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_heavy,
|
||||
a.leading_tight,
|
||||
gtMobile && a.text_lg,
|
||||
]}
|
||||
numberOfLines={2}>
|
||||
{info.displayName}
|
||||
</Text>
|
||||
<View style={[a.flex_row, {gap: 6}]}>
|
||||
<Text
|
||||
style={[
|
||||
a.flex_shrink,
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
gtPhone && a.text_sm,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeHandle(info.creatorHandle, '@')}
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_center, {gap: 2}]}>
|
||||
<HeartFilled
|
||||
size="xs"
|
||||
fill={
|
||||
likeUri
|
||||
? t.palette.like
|
||||
: t.atoms.text_contrast_low.color
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
gtPhone && a.text_sm,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{formatCount(i18n, likeCount)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ChevronDown
|
||||
size="md"
|
||||
fill={t.atoms.text_contrast_low.color}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Layout.Header.Content>
|
||||
|
||||
{hasSession && (
|
||||
<Layout.Header.Slot>
|
||||
{isPinned ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Open feed options menu`)}>
|
||||
{({props}) => {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
label={_(msg`Open feed options menu`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
color="secondary">
|
||||
<PinFilled size="lg" fill={t.palette.primary_500} />
|
||||
</Button>
|
||||
)
|
||||
}}
|
||||
</Menu.Trigger>
|
||||
|
||||
<Menu.Outer>
|
||||
<Menu.Item
|
||||
disabled={isFeedStateChangePending}
|
||||
label={_(msg`Unpin from home`)}
|
||||
onPress={onTogglePinned}>
|
||||
<Menu.ItemText>{_(msg`Unpin from home`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={X} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
disabled={isFeedStateChangePending}
|
||||
label={
|
||||
isSaved
|
||||
? _(msg`Remove from my feeds`)
|
||||
: _(msg`Save to my feeds`)
|
||||
}
|
||||
onPress={onToggleSaved}>
|
||||
<Menu.ItemText>
|
||||
{isSaved
|
||||
? _(msg`Remove from my feeds`)
|
||||
: _(msg`Save to my feeds`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isSaved ? Trash : Plus}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
) : (
|
||||
<Button
|
||||
label={_(msg`Pin to Home`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
color="secondary"
|
||||
onPress={onTogglePinned}>
|
||||
<ButtonIcon icon={Pin} size="lg" />
|
||||
</Button>
|
||||
)}
|
||||
</Layout.Header.Slot>
|
||||
)}
|
||||
</Layout.Header.Outer>
|
||||
</Layout.Center>
|
||||
|
||||
<Dialog.Outer control={infoControl}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Feed menu`)}
|
||||
style={[gtMobile ? {width: 'auto', minWidth: 450} : a.w_full]}>
|
||||
<DialogInner
|
||||
info={info}
|
||||
likeUri={likeUri}
|
||||
setLikeUri={setLikeUri}
|
||||
likeCount={likeCount}
|
||||
isPinned={isPinned}
|
||||
onTogglePinned={onTogglePinned}
|
||||
isFeedStateChangePending={isFeedStateChangePending}
|
||||
/>
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogInner({
|
||||
info,
|
||||
likeUri,
|
||||
setLikeUri,
|
||||
likeCount,
|
||||
isPinned,
|
||||
onTogglePinned,
|
||||
isFeedStateChangePending,
|
||||
}: {
|
||||
info: FeedSourceFeedInfo
|
||||
likeUri: string
|
||||
setLikeUri: (uri: string) => void
|
||||
likeCount: number
|
||||
isPinned: boolean
|
||||
onTogglePinned: () => void
|
||||
isFeedStateChangePending: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
const playHaptic = useHaptics()
|
||||
const control = Dialog.useDialogContext()
|
||||
const reportDialogControl = useReportDialogControl()
|
||||
const [rt] = useRichText(info.description.text)
|
||||
const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
|
||||
const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
|
||||
useUnlikeMutation()
|
||||
|
||||
const isLiked = !!likeUri
|
||||
const feedRkey = React.useMemo(() => new AtUri(info.uri).rkey, [info.uri])
|
||||
|
||||
const onToggleLiked = React.useCallback(async () => {
|
||||
try {
|
||||
playHaptic()
|
||||
|
||||
if (isLiked && likeUri) {
|
||||
await unlikeFeed({uri: likeUri})
|
||||
setLikeUri('')
|
||||
} else {
|
||||
const res = await likeFeed({uri: info.uri, cid: info.cid})
|
||||
setLikeUri(res.uri)
|
||||
}
|
||||
} catch (err) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg`There was an issue contacting the server, please check your internet connection and try again.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
logger.error('Failed to toggle like', {message: err})
|
||||
}
|
||||
}, [playHaptic, isLiked, likeUri, unlikeFeed, setLikeUri, likeFeed, info, _])
|
||||
|
||||
const onPressShare = React.useCallback(() => {
|
||||
playHaptic()
|
||||
const url = toShareUrl(info.route.href)
|
||||
shareUrl(url)
|
||||
}, [info, playHaptic])
|
||||
|
||||
const onPressReport = React.useCallback(() => {
|
||||
reportDialogControl.open()
|
||||
}, [reportDialogControl])
|
||||
|
||||
return (
|
||||
<View style={[a.gap_md]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
||||
<UserAvatar type="algo" size={48} avatar={info.avatar} />
|
||||
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<Text
|
||||
style={[a.text_2xl, a.font_heavy, a.leading_tight]}
|
||||
numberOfLines={2}>
|
||||
{info.displayName}
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_tight, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
<Trans>
|
||||
By{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`View ${info.creatorHandle}'s profile`)}
|
||||
to={makeProfileLink({
|
||||
did: info.creatorDid,
|
||||
handle: info.creatorHandle,
|
||||
})}
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.leading_tight,
|
||||
a.underline,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
onPress={() => control.close()}>
|
||||
{sanitizeHandle(info.creatorHandle, '@')}
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
label={_(msg`Share this feed`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={onPressShare}>
|
||||
<ButtonIcon icon={Share} size="lg" />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<RichText value={rt} style={[a.text_md, a.leading_snug]} />
|
||||
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
|
||||
{typeof likeCount === 'number' && (
|
||||
<InlineLinkText
|
||||
label={_(msg`View users who like this feed`)}
|
||||
to={makeCustomFeedLink(info.creatorDid, feedRkey, 'liked-by')}
|
||||
style={[a.underline, t.atoms.text_contrast_medium]}
|
||||
onPress={() => control.close()}>
|
||||
<Trans>
|
||||
Liked by <Plural value={likeCount} one="# user" other="# users" />
|
||||
</Trans>
|
||||
</InlineLinkText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center, a.pt_sm]}>
|
||||
<Button
|
||||
disabled={isLikePending || isUnlikePending}
|
||||
label={_(msg`Like feed`)}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onPress={onToggleLiked}
|
||||
style={[a.flex_1]}>
|
||||
{isLiked ? (
|
||||
<HeartFilled size="sm" fill={t.palette.like} />
|
||||
) : (
|
||||
<ButtonIcon icon={Heart} position="left" />
|
||||
)}
|
||||
|
||||
<ButtonText>
|
||||
{isLiked ? <Trans>Unlike</Trans> : <Trans>Like</Trans>}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isFeedStateChangePending}
|
||||
label={isPinned ? _(msg`Unpin feed`) : _(msg`Pin feed`)}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color={isPinned ? 'secondary' : 'primary'}
|
||||
onPress={onTogglePinned}
|
||||
style={[a.flex_1]}>
|
||||
<ButtonText>
|
||||
{isPinned ? <Trans>Unpin feed</Trans> : <Trans>Pin feed</Trans>}
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={Pin} position="right" />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View style={[a.pt_xs, a.gap_lg]}>
|
||||
<Divider />
|
||||
|
||||
<View
|
||||
style={[a.flex_row, a.align_center, a.gap_sm, a.justify_between]}>
|
||||
<Text style={[a.italic, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Something wrong? Let us know.</Trans>
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
label={_(msg`Report feed`)}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onPress={onPressReport}>
|
||||
<ButtonText>
|
||||
<Trans>Report feed</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={CircleInfo} position="right" />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<ReportDialog
|
||||
control={reportDialogControl}
|
||||
params={{
|
||||
type: 'feedgen',
|
||||
uri: info.uri,
|
||||
cid: info.cid,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import {useLingui} from '@lingui/react'
|
||||
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||
import {IS_INTERNAL} from '#/lib/app-info'
|
||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
|
||||
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
|
||||
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
|
||||
@@ -23,7 +23,7 @@ export function AppIconSettingsScreen({}: Props) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const sets = useAppIconSets()
|
||||
const {currentAccount} = useSession()
|
||||
const gate = useGate()
|
||||
const [currentAppIcon, setCurrentAppIcon] = useState(() =>
|
||||
getAppIconName(DynamicAppIcon.getAppIcon()),
|
||||
)
|
||||
@@ -86,7 +86,7 @@ export function AppIconSettingsScreen({}: Props) {
|
||||
))}
|
||||
</Group>
|
||||
|
||||
{DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
|
||||
{IS_INTERNAL && gate('debug_subscriptions') && (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
|
||||
@@ -8,10 +8,10 @@ import Animated, {
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||
import {IS_INTERNAL} from '#/lib/app-info'
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
|
||||
import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
|
||||
import {atoms as a, native, useAlf, useTheme} from '#/alf'
|
||||
@@ -29,6 +29,7 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
|
||||
export function AppearanceSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const {fonts} = useAlf()
|
||||
const gate = useGate()
|
||||
|
||||
const {colorMode, darkTheme} = useThemePrefs()
|
||||
const {setColorMode, setDarkTheme} = useSetThemePrefs()
|
||||
@@ -74,8 +75,6 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||
[fonts],
|
||||
)
|
||||
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
return (
|
||||
<LayoutAnimationConfig skipExiting skipEntering>
|
||||
<Layout.Screen testID="preferencesThreadsScreen">
|
||||
@@ -178,7 +177,7 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||
onChange={onChangeFontScale}
|
||||
/>
|
||||
|
||||
{isNative && DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
|
||||
{isNative && IS_INTERNAL && gate('debug_subscriptions') && (
|
||||
<>
|
||||
<SettingsList.Divider />
|
||||
<AppIconSettingsListItem />
|
||||
|
||||
@@ -18,7 +18,13 @@ type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationSettings'>
|
||||
export function NotificationSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
|
||||
const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
|
||||
const {
|
||||
data,
|
||||
isError: isQueryError,
|
||||
refetch,
|
||||
} = useNotificationFeedQuery({
|
||||
filter: 'all',
|
||||
})
|
||||
const serverPriority = data?.pages.at(0)?.priority
|
||||
|
||||
const {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {createFullHandle, validateHandle} from '#/lib/strings/handles'
|
||||
import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
|
||||
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
|
||||
@@ -29,7 +30,10 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
|
||||
import {
|
||||
ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon,
|
||||
ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon,
|
||||
} from '#/components/icons/Arrow'
|
||||
import {At_Stroke2_Corner0_Rounded as AtIcon} from '#/components/icons/At'
|
||||
import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
|
||||
@@ -245,15 +249,14 @@ function ProvidedHandlePage({
|
||||
<Text style={[a.leading_snug]}>
|
||||
<Trans>
|
||||
If you have your own domain, you can use that as your handle. This
|
||||
lets you self-verify your identity –{' '}
|
||||
lets you self-verify your identity.{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`learn more`)}
|
||||
to="https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial"
|
||||
style={[a.font_bold]}
|
||||
disableMismatchWarning>
|
||||
learn more
|
||||
Learn more here.
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
@@ -265,7 +268,7 @@ function ProvidedHandlePage({
|
||||
<ButtonText>
|
||||
<Trans>I have my own domain</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={ArrowRightIcon} />
|
||||
<ButtonIcon icon={ArrowRightIcon} position="right" />
|
||||
</Button>
|
||||
</Animated.View>
|
||||
</View>
|
||||
@@ -488,6 +491,18 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
|
||||
</Animated.View>
|
||||
)}
|
||||
<Animated.View layout={native(LinearTransition)}>
|
||||
{currentAccount?.handle?.endsWith('.bsky.social') && (
|
||||
<Admonition type="info" style={[a.mb_md]}>
|
||||
<Trans>
|
||||
Your current handle{' '}
|
||||
<Text style={[a.font_bold]}>
|
||||
{sanitizeHandle(currentAccount?.handle || '', '@')}
|
||||
</Text>{' '}
|
||||
will automatically remain reserved for you. You can switch back to
|
||||
it at any time from this account.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
)}
|
||||
<Button
|
||||
label={
|
||||
isVerified
|
||||
@@ -498,7 +513,7 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
|
||||
}
|
||||
variant="solid"
|
||||
size="large"
|
||||
color={domain.trim().length > 0 ? 'primary' : 'secondary'}
|
||||
color="primary"
|
||||
disabled={domain.trim().length === 0}
|
||||
onPress={() => {
|
||||
if (isVerified) {
|
||||
@@ -521,14 +536,17 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
|
||||
</ButtonText>
|
||||
)}
|
||||
</Button>
|
||||
</Animated.View>
|
||||
<Animated.View layout={native(LinearTransition)}>
|
||||
|
||||
<Button
|
||||
label={_(msg`Use default provider`)}
|
||||
accessibilityHint={_(msg`Go back to previous page`)}
|
||||
onPress={goToServiceHandle}
|
||||
style={[a.p_0, a.justify_start]}>
|
||||
<ButtonText style={[{color: t.palette.primary_500}, a.text_left]}>
|
||||
variant="outline"
|
||||
color="secondary"
|
||||
size="large"
|
||||
style={[a.mt_sm]}>
|
||||
<ButtonIcon icon={ArrowLeftIcon} position="left" />
|
||||
<ButtonText>
|
||||
<Trans>Nevermind, create a handle for me</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
|
||||
@@ -111,7 +111,7 @@ export function StepHandle() {
|
||||
handleValueRef.current = val
|
||||
setDraftValue(val)
|
||||
}}
|
||||
label={_(msg`Input your user handle`)}
|
||||
label={_(msg`Type your desired username`)}
|
||||
defaultValue={draftValue}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
@@ -122,7 +122,7 @@ export function StepHandle() {
|
||||
</View>
|
||||
{draftValue !== '' && (
|
||||
<Text style={[a.text_md]}>
|
||||
<Trans>Your full handle will be</Trans>{' '}
|
||||
<Trans>Your full username will be</Trans>{' '}
|
||||
<Text style={[a.text_md, a.font_bold]}>
|
||||
@{createFullHandle(draftValue, state.userDomain)}
|
||||
</Text>
|
||||
|
||||
@@ -139,7 +139,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||
{state.activeStep === SignupStep.INFO ? (
|
||||
<Trans>Your account</Trans>
|
||||
) : state.activeStep === SignupStep.HANDLE ? (
|
||||
<Trans>Your user handle</Trans>
|
||||
<Trans>Choose your username</Trans>
|
||||
) : (
|
||||
<Trans>Complete the challenge</Trans>
|
||||
)}
|
||||
|
||||
@@ -407,6 +407,7 @@ function Header({
|
||||
isOwner={isOwn}
|
||||
avatar={undefined}
|
||||
creator={creator}
|
||||
purpose="app.bsky.graph.defs#referencelist"
|
||||
avatarType="starter-pack">
|
||||
{hasSession ? (
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
|
||||
|
||||
@@ -3,7 +3,6 @@ import EventEmitter from 'eventemitter3'
|
||||
|
||||
import {networkRetry} from '#/lib/async/retry'
|
||||
import {logger} from '#/logger'
|
||||
import {IS_DEV} from '#/env'
|
||||
import {Device, device} from '#/storage'
|
||||
|
||||
const events = new EventEmitter()
|
||||
@@ -65,7 +64,7 @@ export function beginResolveGeolocation() {
|
||||
* In dev, IP server is unavailable, so we just set the default geolocation
|
||||
* and fail closed.
|
||||
*/
|
||||
if (IS_DEV) {
|
||||
if (__DEV__) {
|
||||
geolocationResolution = new Promise(y => y())
|
||||
device.set(['geolocation'], DEFAULT_GEOLOCATION)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
|
||||
type StateContext = boolean
|
||||
type ApiContext = (hasNew: boolean) => void
|
||||
|
||||
const stateContext = React.createContext<StateContext>(false)
|
||||
const apiContext = React.createContext<ApiContext>((_: boolean) => {})
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const [state, setState] = React.useState(false)
|
||||
return (
|
||||
<stateContext.Provider value={state}>
|
||||
<apiContext.Provider value={setState}>{children}</apiContext.Provider>
|
||||
</stateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useHomeBadge() {
|
||||
return React.useContext(stateContext)
|
||||
}
|
||||
|
||||
export function useSetHomeBadge() {
|
||||
return React.useContext(apiContext)
|
||||
}
|
||||
@@ -24,6 +24,7 @@ const _emitter = new EventEmitter()
|
||||
|
||||
export async function init() {
|
||||
broadcast.onmessage = onBroadcastMessage
|
||||
window.onstorage = onStorage
|
||||
const stored = readFromStorage()
|
||||
if (stored) {
|
||||
_state = stored
|
||||
@@ -90,6 +91,17 @@ export async function clearStorage() {
|
||||
}
|
||||
clearStorage satisfies PersistedApi['clearStorage']
|
||||
|
||||
function onStorage() {
|
||||
const next = readFromStorage()
|
||||
if (next === _state) {
|
||||
return
|
||||
}
|
||||
if (next) {
|
||||
_state = next
|
||||
_emitter.emit('update')
|
||||
}
|
||||
}
|
||||
|
||||
async function onBroadcastMessage({data}: MessageEvent) {
|
||||
if (
|
||||
typeof data === 'object' &&
|
||||
|
||||
@@ -52,25 +52,22 @@ const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
|
||||
const RQKEY_ROOT = 'notification-feed'
|
||||
export function RQKEY(priority?: false) {
|
||||
return [RQKEY_ROOT, priority]
|
||||
export function RQKEY(filter: 'all' | 'mentions') {
|
||||
return [RQKEY_ROOT, filter]
|
||||
}
|
||||
|
||||
export function useNotificationFeedQuery(opts?: {
|
||||
export function useNotificationFeedQuery(opts: {
|
||||
enabled?: boolean
|
||||
overridePriorityNotifications?: boolean
|
||||
filter: 'all' | 'mentions'
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const unreads = useUnreadNotificationsApi()
|
||||
const enabled = opts?.enabled !== false
|
||||
const enabled = opts.enabled !== false
|
||||
const filter = opts.filter
|
||||
const {uris: hiddenReplyUris} = useThreadgateHiddenReplyUris()
|
||||
|
||||
// false: force showing all notifications
|
||||
// undefined: let the server decide
|
||||
const priority = opts?.overridePriorityNotifications ? false : undefined
|
||||
|
||||
const selectArgs = useMemo(() => {
|
||||
return {
|
||||
moderationOpts,
|
||||
@@ -91,14 +88,23 @@ export function useNotificationFeedQuery(opts?: {
|
||||
RQPageParam
|
||||
>({
|
||||
staleTime: STALE.INFINITY,
|
||||
queryKey: RQKEY(priority),
|
||||
queryKey: RQKEY(filter),
|
||||
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||
let page
|
||||
if (!pageParam) {
|
||||
if (filter === 'all' && !pageParam) {
|
||||
// for the first page, we check the cached page held by the unread-checker first
|
||||
page = unreads.getCachedUnreadPage()
|
||||
}
|
||||
if (!page) {
|
||||
let reasons: string[] = []
|
||||
if (filter === 'mentions') {
|
||||
reasons = [
|
||||
// Anything that's a post
|
||||
'mention',
|
||||
'reply',
|
||||
'quote',
|
||||
]
|
||||
}
|
||||
const {page: fetchedPage} = await fetchPage({
|
||||
agent,
|
||||
limit: PAGE_SIZE,
|
||||
@@ -106,13 +112,13 @@ export function useNotificationFeedQuery(opts?: {
|
||||
queryClient,
|
||||
moderationOpts,
|
||||
fetchAdditionalData: true,
|
||||
priority,
|
||||
reasons,
|
||||
})
|
||||
page = fetchedPage
|
||||
}
|
||||
|
||||
// if the first page has an unread, mark all read
|
||||
if (!pageParam) {
|
||||
if (filter === 'all' && !pageParam) {
|
||||
// if the first page has an unread, mark all read
|
||||
unreads.markAllRead()
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ export function useNotificationSettingsMutation() {
|
||||
},
|
||||
onSettled: () => {
|
||||
invalidateCachedUnreadPage()
|
||||
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()})
|
||||
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')})
|
||||
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -54,7 +55,7 @@ function eagerlySetCachedPriority(
|
||||
queryClient: ReturnType<typeof useQueryClient>,
|
||||
enabled: boolean,
|
||||
) {
|
||||
queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => {
|
||||
function updateData(old: any) {
|
||||
if (!old) return old
|
||||
return {
|
||||
...old,
|
||||
@@ -65,5 +66,7 @@ function eagerlySetCachedPriority(
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData)
|
||||
queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user