Compare commits

...

38 Commits

Author SHA1 Message Date
Dan Abramov 493a4928f9 Refetch empty feed on focus 2024-12-17 10:45:19 +00:00
dan 20d4266e2f Read storage on window.onstorage (#7137) 2024-12-17 03:13:51 +00:00
dan c9e429760d [ELI5] Tweak wording on the signup screen (#7136)
* Tweak signup wordings

* Update translations
2024-12-17 00:11:21 +00:00
Samuel Newman be33c626b0 alf error screen (#7135) 2024-12-17 00:11:03 +00:00
Samuel Newman 1a160df396 add safe area view to profile error screen (#7134) 2024-12-17 00:02:56 +00:00
dan de97c07c63 Adjust gates (#7132)
* Remove dead gate

* Adjust gates

* No need to disable exposures
2024-12-16 22:26:40 +00:00
Samuel Newman c339dc5b05 disable automaticallAdjustsScrollIndicatorInsets (#7131) 2024-12-16 22:06:15 +00:00
dan 3ed237b629 Bump more native deps (#7129)
* Undo expo modules patch

* Upgrade expo modules release

* Patch facebook/react-native#46944

* Remove explicit prebuild config dep

* Bump to rm duplicates
2024-12-16 21:46:39 +00:00
dan 21c288272d Update more Expo packages (#7127)
* Bump Expo resolutions

* Bump packages

* Bumpy bump
2024-12-16 17:35:44 +00:00
Karol Stawowski c4fffba11b feat: widen recent search profile link for mobile devices (#7119)
* feat: widen recent search profile link for mobile devices

* chore: apply style to non-mobile screens
2024-12-16 16:53:00 +00:00
dan e4e5a589b1 Fix video uploads on native (#7126)
* Fix duplicate expo-modules-core

* Patch expo/expo#33621
2024-12-16 15:42:29 +00:00
Frudrax Cheng b1b6affb71 Fix post time localization on Android (#6742)
* Fix post time localization on native

* Fix timezone

* Revent time.ts

* Add time.android.ts

* Add missing 'an' and 'ast'

* Revent PostThreadItem.tsx

* Resolve duplicate versions in lockfile

* Move to time.native.ts

* Follow #7055 to upgrade intl-datetimeformat version

* Remove time.native.ts
2024-12-16 14:25:59 +00:00
dan 1ac307bc42 [Experiment] Remove "Load Latest" button (#7120)
* Remove "show latest" behind the gate

* Add HomeBadgeProvider

* Update provider state from home feed tabs

* Add Home badge to native

* Add Home badge to mobile web

* Add Home badge to desktop web
2024-12-15 20:30:17 +00:00
dan 80c0125d6b Fix image upload on native (#7121) 2024-12-15 17:42:45 +00:00
Eric Bailey c959dc351f ProfileFeed cleanups (#7115)
* Clean up feed load state

* Fix feed info dialog load state
2024-12-14 15:30:01 -06:00
Ben Harris b806f3e85d apply fonts to TextField (#6288)
system fonts were leaking. i'm sure there's a better way to apply this
without duplicating but idk it works
2024-12-14 03:02:12 +00:00
dan c5e60ba006 Fix posting images on web (#7105)
* Fix uploadBlob to handle blob: URI

* Patch expo-image-manipulator to support "compress" argument

* Narrow down the fix
2024-12-14 02:25:32 +00:00
dan 55222c5b0e Fix z-indexes to make tabbar scroll on Android (#7102) 2024-12-14 00:01:31 +00:00
Eric Bailey ca8ca903b2 Add some helper text for custom domains (#7103)
* Add some helper text for custom domains

* endsWith

* ope
2024-12-13 23:05:53 +00:00
dan da6bcc54cf Tweak ProfileList design (#7100)
* Remove "No description"

* Move Lists about into header

* Remove pager with one tab

* Layout tweaks
2024-12-13 21:38:43 +00:00
dan adbc27059a Add Emily to discover debug (#7101) 2024-12-13 21:16:25 +00:00
Eric Bailey ce69e052b7 Allow non-graph emoji to inherit parent color (#7096) 2024-12-13 12:36:00 -06:00
dan f6aecf5c28 Give minWidth to tab indicator only (#7095) 2024-12-13 18:20:48 +00:00
Eric Bailey 41228dce19 Better alignment on web (#7098) 2024-12-13 18:20:38 +00:00
dan 356dad1932 Remove the environment indirections (#7089)
* Use raw underlying globals for environment

* Set dev EXPO_PUBLIC_ENV by exclusion
2024-12-13 17:16:53 +00:00
dan e2a7965e43 Gate bitdrift integration (#7088)
* Move Statsig init call earlier

* Gate Bitdrift init call

* Remove IS_TEST env constant

* Mock statsig
2024-12-13 17:14:41 +00:00
Brad Estey 5655241bef Uppercase the auth factor token in input value to match the email casing. (#7094) 2024-12-13 17:14:31 +00:00
dan add7a91d54 Fix web Home header (#7086)
* Fix header disappearing on web

* Make border nicer
2024-12-12 21:57:26 +00:00
dan 8a790cdd7f Revert "Edit string to avoid churning translations (#6737)" (#7087)
This reverts commit 9d3bdbb2b9.
2024-12-12 21:52:51 +00:00
surfdude29 10dd0f76eb mark string for localization (#7083) 2024-12-12 21:50:30 +00:00
dan 6e20893a0c Fix double border due to progress guide (#7084) 2024-12-12 15:16:15 -06:00
Hailey 2276cb0e75 Fix share button not working for some users (#5849)
* fix share button

* Revert "fix share button"

This reverts commit 3521c241729dc9bbe3dd7b62fc6e3e61e011cdf9.

* tweak

* Clean up context

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-12-12 13:50:29 -06:00
Eric Bailey 2808f8b73d New profile feed header (#7056)
* Init hacking

* Lil baby button checkpoint

* Playing around

* Revert "Playing around"

This reverts commit f58a7fafa1.

* Mostly there

* Cleanups

* Cleanup

* Fix report dialog nesting

* Remove transform on native

* Rename header

* Fix layout, overflowing FAB buttons

* Remove hack

* Couple of fixes

* Keep Pin primary CTA (#7061)

* Update src/screens/Profile/components/ProfileFeedHeader.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Simplify, use old string

* Wrap Trans better

---------

Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-12-12 11:48:26 -06:00
Samuel Newman ffc63dc85f [Layout] Bleed profile banner into safe area (#6967)
* bleed profile banner into safe area

(cherry picked from commit 50b3a4d0c6fd94b583ffe4efa65de35c81ae7f4e)

* pointer events none when hidden

(cherry picked from commit bae2c7b2dd6d7f858a98812196628308c0877755)

* fix web

(cherry picked from commit e3f9597170375f2903b6e567b963f008ec95aed1)

* add status bar shadow

* rm log

* rm mini header

* speed up animation

* pass bool rather than int in light status bar
2024-12-12 17:46:19 +00:00
Samuel Newman 4b32b0a71b Increase scrubber height if touch device (#7081)
* further increase target area of scrubber

* make scrubber bigger if touch device

---------

Co-authored-by: Antonio Sarcevic <sarcevicantonio@gmail.com>
2024-12-12 17:42:36 +00:00
dan f8cdd6b9ae [Notifications] Add a Mentions tab (#7044)
* Split out NotificationsTab

* Remove unused route parameter

* Refine the split between components

* Hoist some logic out of NotificationFeed

* Remove unused option

* Add all|conversations to query, hardcode "all"

* Add a Conversations tab

* Rename to Mentions

* Bump packages

* Rename fields

* Fix oopsie

* Simplify header

* Track active tab

* Fix types

* Separate logic for tabs

* Better border for first unread

* Highlight unread for all only

* Fix spinner races

* Fix fetchPage races

* Fix bottom bar border being obscured by glimmer

* Remember last tab within the session

* One tab at a time

* Fix TS

* Handle all RQKEY usages

* Nit
2024-12-12 17:37:07 +00:00
Samuel Newman 10e241e7eb followersCount -> followsCount (#7080) 2024-12-12 17:21:28 +00:00
dan 88166926fa Make the pager take full width (#7066)
* Wide tabs for web

* Wide tabs on mobile

* Tweak min for profile

* Driveby border fix

* Fix single tab indicator
2024-12-12 15:35:56 +00:00
138 changed files with 2699 additions and 1812 deletions
-1
View File
@@ -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=
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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

+1 -2
View File
@@ -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
View File
@@ -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": {
+101
View File
@@ -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);
});
}
+36 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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'
+4
View File
@@ -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]}%)`,
+1
View File
@@ -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
+1 -3
View File
@@ -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,
+3 -19
View File
@@ -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
)
}
+2 -1
View File
@@ -175,7 +175,8 @@ export function TitleText({
gtMobile && a.text_xl,
style,
]}
numberOfLines={2}>
numberOfLines={2}
emoji>
{children}
</Text>
)
+3 -1
View File
@@ -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}
/>
</>
+20
View File
@@ -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
}
+18 -18
View File
@@ -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>
+10 -9
View File
@@ -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={[
+1 -2
View File
@@ -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>
+2 -3
View File
@@ -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()
}
+53 -30
View File
@@ -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
+2 -2
View File
@@ -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
+4
View File
@@ -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',
})
-3
View File
@@ -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'
+4 -1
View File
@@ -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
View File
@@ -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'
})`
+3 -5
View File
@@ -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
View File
@@ -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'})
}
})
-9
View File
@@ -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')
}
},
+11 -4
View File
@@ -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()
}
})
+3 -3
View File
@@ -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
View File
@@ -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 -2
View File
@@ -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'
+2 -3
View File
@@ -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),
+29
View File
@@ -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'),
])
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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 ""
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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 ""
+3 -3
View File
@@ -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 ""
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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 dactu 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"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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 "आपका उपयोगकर्ता हैंडल"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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 "あなたのユーザーハンドル"
+3 -3
View File
@@ -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 "내 사용자 핸들"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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 "Ваш псевдоним"
+3 -3
View File
@@ -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 "แฮนด์เดิลผู้ใช้ของคุณ"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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 "Ваш псевдонім"
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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 "你的账户代码"
+3 -3
View File
@@ -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 "你嘅帳號頭銜"
+3 -3
View File
@@ -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 "您的帳號代碼"
+2 -2
View File
@@ -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
-3
View File
@@ -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
View File
@@ -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)
}
}
+2 -2
View File
@@ -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>
)}
+10
View File
@@ -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]}>
+1 -2
View File
@@ -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"
+12 -3
View File
@@ -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'}
+19 -12
View File
@@ -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
}
+112 -7
View File
@@ -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',
+238
View File
@@ -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,
},
})
+1 -1
View File
@@ -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={[
+4 -5
View File
@@ -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>
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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]}>
+1 -2
View File
@@ -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
+24
View File
@@ -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)
}
+12
View File
@@ -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' &&
+20 -14
View File
@@ -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()
}
+6 -3
View File
@@ -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