Compare commits
24 Commits
explore/refetch
...
lbl
| Author | SHA1 | Date | |
|---|---|---|---|
| 238b00d193 | |||
| bf69672674 | |||
| 5371114609 | |||
| 4c0213b3e9 | |||
| 02a05d9948 | |||
| 4c142a86b4 | |||
| f3153b8acb | |||
| a59fb91a99 | |||
| c8568e3004 | |||
| 09111ef272 | |||
| b9ed94c7f4 | |||
| 872fcd1d20 | |||
| d496b5bdbf | |||
| 4879a07701 | |||
| 28876a4b23 | |||
| 1e8a96e297 | |||
| 815a1e4c44 | |||
| 4013855c10 | |||
| e46b78eb85 | |||
| 94fd12ff49 | |||
| a181b0b935 | |||
| a301e31f76 | |||
| 502d660eb4 | |||
| 6840fbb237 |
@@ -139,7 +139,7 @@ jobs:
|
||||
|
||||
- name: 🏗️ Build Production APK
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
run: yarn use-build-number-with-bump eas build -p android --profile production-apk --local --output build.apk --non-interactive
|
||||
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} yarn use-build-number-with-bump eas build -p android --profile production-apk --local --output build.apk --non-interactive
|
||||
|
||||
- name: 🚀 Upload Production APK Artifact
|
||||
id: upload-artifact-production-apk
|
||||
|
||||
@@ -17,7 +17,6 @@ appId: xyz.blueskyweb.app
|
||||
- inputText: "Post with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn: "Allow Full Access"
|
||||
- tapOn: "Content warnings"
|
||||
- tapOn: "Porn"
|
||||
- tapOn:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Internationalization
|
||||
|
||||
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please get involved on [Crowdin](https://bluesky.crowdin.com/u/projects/1) or open an issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app).
|
||||
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please get involved on [Crowdin](https://bluesky.crowdin.com/bluesky-social) or open an issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app).
|
||||
|
||||
## Tools
|
||||
|
||||
- We use Crowdin to manage translations.
|
||||
- Bluesky Crowdin: https://bluesky.crowdin.com/u/projects/1
|
||||
- Bluesky Crowdin: https://bluesky.crowdin.com/bluesky-social
|
||||
- Introduction to Crowdin: https://support.crowdin.com/for-translators/
|
||||
- We use Lingui to implement translations. You can find the documentation [here](https://lingui.dev/).
|
||||
|
||||
@@ -15,7 +15,7 @@ Much of the app is translated by community contributions. (We <3 our translators
|
||||
|
||||
### Using Crowdin
|
||||
|
||||
[Crowdin](https://bluesky.crowdin.com/u/projects/1) is our primary tool for managing translations. There are two roles:
|
||||
[Crowdin](https://bluesky.crowdin.com/bluesky-social) is our primary tool for managing translations. There are two roles:
|
||||
|
||||
- **Proof-readers**. Can create new translations and approve submitted translations.
|
||||
- **Translators**. Can create new translations.
|
||||
@@ -32,7 +32,7 @@ Please treat everyone with respect. Proof-readers are given final say on transla
|
||||
|
||||
### Adding a new language
|
||||
|
||||
You can request a new language be added to the project by clicking **Request New Language** on Crowdin or you can create a [GitHub issue](https://github.com/bluesky-social/social-app/issues).
|
||||
You can request a new language be added to the project by clicking **Request New Language** on [Crowdin](https://bluesky.crowdin.com/bluesky-social) or you can create a [GitHub issue](https://github.com/bluesky-social/social-app/issues).
|
||||
|
||||
Please only request a new language when you are certain you will be able to contribute a substantive portion of translations for the language.
|
||||
|
||||
|
||||
+44
-44
@@ -58,7 +58,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.14.20",
|
||||
"@atproto/api": "^0.14.21",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -86,12 +86,12 @@
|
||||
"@radix-ui/react-focus-scope": "^1.1.2",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-menu/menu": "^1.1.7",
|
||||
"@react-native-picker/picker": "2.10.2",
|
||||
"@react-native-picker/picker": "2.10.3",
|
||||
"@react-navigation/bottom-tabs": "^6.5.20",
|
||||
"@react-navigation/drawer": "^6.6.15",
|
||||
"@react-navigation/native": "^6.1.17",
|
||||
"@react-navigation/native-stack": "^6.9.26",
|
||||
"@sentry/react-native": "6.9.1",
|
||||
"@sentry/react-native": "~6.10.0",
|
||||
"@tanstack/query-async-storage-persister": "^5.25.0",
|
||||
"@tanstack/react-query": "^5.8.1",
|
||||
"@tanstack/react-query-persist-client": "^5.25.0",
|
||||
@@ -123,35 +123,35 @@
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "~52.0.19",
|
||||
"expo-application": "^6.0.1",
|
||||
"expo": "~52.0.42",
|
||||
"expo-application": "~6.0.2",
|
||||
"expo-blur": "^14.0.3",
|
||||
"expo-build-properties": "^0.13.1",
|
||||
"expo-camera": "~16.0.7",
|
||||
"expo-clipboard": "^7.0.0",
|
||||
"expo-dev-client": "^5.0.4",
|
||||
"expo-device": "~7.0.1",
|
||||
"expo-file-system": "^18.0.6",
|
||||
"expo-font": "~13.0.1",
|
||||
"expo-haptics": "^14.0.0",
|
||||
"expo-image": "~2.0.3",
|
||||
"expo-image-manipulator": "^13.0.5",
|
||||
"expo-image-picker": "~16.0.3",
|
||||
"expo-linear-gradient": "^14.0.1",
|
||||
"expo-linking": "^7.0.3",
|
||||
"expo-localization": "~16.0.0",
|
||||
"expo-media-library": "~17.0.3",
|
||||
"expo-navigation-bar": "~4.0.4",
|
||||
"expo-notifications": "~0.29.11",
|
||||
"expo-screen-orientation": "^8.0.2",
|
||||
"expo-sharing": "^13.0.0",
|
||||
"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.10",
|
||||
"expo-video": "^2.0.5",
|
||||
"expo-web-browser": "~14.0.1",
|
||||
"expo-build-properties": "~0.13.2",
|
||||
"expo-camera": "~16.0.18",
|
||||
"expo-clipboard": "~7.0.1",
|
||||
"expo-dev-client": "~5.0.19",
|
||||
"expo-device": "~7.0.3",
|
||||
"expo-file-system": "~18.0.12",
|
||||
"expo-font": "~13.0.4",
|
||||
"expo-haptics": "~14.0.1",
|
||||
"expo-image": "~2.0.7",
|
||||
"expo-image-manipulator": "~13.0.6",
|
||||
"expo-image-picker": "~16.0.6",
|
||||
"expo-linear-gradient": "~14.0.2",
|
||||
"expo-linking": "~7.0.5",
|
||||
"expo-localization": "~16.0.1",
|
||||
"expo-media-library": "~17.0.6",
|
||||
"expo-navigation-bar": "~4.0.9",
|
||||
"expo-notifications": "~0.29.14",
|
||||
"expo-screen-orientation": "~8.0.4",
|
||||
"expo-sharing": "~13.0.1",
|
||||
"expo-splash-screen": "~0.29.22",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-system-ui": "~4.0.9",
|
||||
"expo-task-manager": "~12.0.6",
|
||||
"expo-updates": "~0.27.4",
|
||||
"expo-video": "~2.0.6",
|
||||
"expo-web-browser": "~14.0.2",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
"hls.js": "^1.5.11",
|
||||
@@ -169,12 +169,12 @@
|
||||
"patch-package": "^6.5.1",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"psl": "^1.9.0",
|
||||
"react": "18.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-compiler-runtime": "19.0.0-beta-a7bf2bd-20241110",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-keyed-flatten-children": "^3.0.0",
|
||||
"react-native": "0.76.6",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-compressor": "1.10.3",
|
||||
"react-native-date-picker": "^5.0.7",
|
||||
"react-native-drawer-layout": "^4.1.1",
|
||||
@@ -185,15 +185,15 @@
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.14.5",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.7.0",
|
||||
"react-native-pager-view": "6.5.1",
|
||||
"react-native-picker-select": "^9.3.1",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
"react-native-reanimated": "3.17.0-nightly-20241211-17e89ca24",
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-root-siblings": "^4.1.1",
|
||||
"react-native-safe-area-context": "4.14.0",
|
||||
"react-native-screens": "~4.3.0",
|
||||
"react-native-svg": "^15.8.0",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-svg": "15.8.0",
|
||||
"react-native-uitextview": "^1.4.0",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-uuid": "^2.0.2",
|
||||
@@ -201,7 +201,7 @@
|
||||
"react-native-vision-camera": "^4.6.3",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-web-webview": "^1.0.2",
|
||||
"react-native-webview": "13.12.3",
|
||||
"react-native-webview": "13.12.5",
|
||||
"react-remove-scroll-bar": "^2.3.6",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
@@ -223,8 +223,8 @@
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@react-native/eslint-config": "^0.76.2",
|
||||
"@react-native/typescript-config": "^0.76.1",
|
||||
"@react-native/eslint-config": "^0.76.9",
|
||||
"@react-native/typescript-config": "^0.76.9",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^12.8.1",
|
||||
@@ -257,7 +257,7 @@
|
||||
"husky": "^8.0.3",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "^52.0.2",
|
||||
"jest-expo": "~52.0.6",
|
||||
"jest-junit": "^16.0.0",
|
||||
"lint-staged": "^13.2.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
@@ -273,7 +273,7 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@expo/image-utils": "0.6.3",
|
||||
"@react-native/babel-preset": "0.76.1",
|
||||
"@react-native/babel-preset": "0.76.9",
|
||||
"@react-native/normalize-colors": "0.76.1",
|
||||
"@types/react": "^18",
|
||||
"**/expo-constants": "17.0.3",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
index b928b8f..7175cf6 100644
|
||||
index 64e6efe..e61485e 100644
|
||||
--- a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
+++ b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
@@ -99,12 +99,12 @@ class MediaLibraryModule : Module() {
|
||||
@@ -111,12 +111,12 @@ class MediaLibraryModule : Module() {
|
||||
}
|
||||
|
||||
AsyncFunction("createAssetAsync") { localUri: String, promise: Promise ->
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
|
||||
index 5dd0c61..bf536dd 100644
|
||||
index 13bffbb..5ebbede 100644
|
||||
--- a/node_modules/expo-notifications/android/build.gradle
|
||||
+++ b/node_modules/expo-notifications/android/build.gradle
|
||||
@@ -46,6 +46,7 @@ dependencies {
|
||||
@@ -41,7 +41,7 @@ diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/Scro
|
||||
index 93af874..106f8ec 100644
|
||||
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
|
||||
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
|
||||
@@ -66,28 +66,51 @@ - (void)preserveContentOffsetWithBlock:(void (^)())block
|
||||
@@ -66,28 +66,51 @@
|
||||
* ScrollView, we force it to be centered, but when you zoom or the content otherwise
|
||||
* becomes larger than the ScrollView, there is no padding around the content but it
|
||||
* can still fill the whole view.
|
||||
@@ -103,7 +103,7 @@ index 93af874..106f8ec 100644
|
||||
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
|
||||
{
|
||||
if ([_overridingDelegate respondsToSelector:@selector(touchesShouldCancelInContentView:)]) {
|
||||
@@ -257,6 +280,10 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
|
||||
@@ -257,6 +280,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont
|
||||
index 53bfd04..ff1b1ed 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
@@ -23,6 +23,7 @@ @implementation RCTRefreshControl {
|
||||
@@ -23,6 +23,7 @@
|
||||
UIColor *_titleColor;
|
||||
CGFloat _progressViewOffset;
|
||||
BOOL _hasMovedToWindow;
|
||||
@@ -139,7 +139,7 @@ index 53bfd04..ff1b1ed 100644
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
@@ -58,6 +59,12 @@ - (void)layoutSubviews
|
||||
@@ -58,6 +59,12 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
_isInitialRender = false;
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ index 53bfd04..ff1b1ed 100644
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
[super didMoveToWindow];
|
||||
@@ -221,4 +228,50 @@ - (void)refreshControlValueChanged
|
||||
@@ -221,4 +228,50 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont
|
||||
index 40aaf9c..1c60164 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
@@ -22,11 +22,12 @@ - (UIView *)view
|
||||
@@ -22,11 +22,12 @@ RCT_EXPORT_MODULE()
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
|
||||
@@ -222,10 +222,10 @@ 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..84a6fca 100644
|
||||
index 6f41b5c..9b4f77f 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
|
||||
@@ -159,26 +159,8 @@
|
||||
return !shouldDisableScrollInteraction;
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ index e9ce48c..84a6fca 100644
|
||||
super.contentOffset = CGPointMake(
|
||||
RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
|
||||
RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
|
||||
@@ -427,6 +409,11 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
|
||||
@@ -433,6 +415,11 @@ static inline void RCTApplyTransformationAccordingLayoutDirection(
|
||||
// Does nothing
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ index e9ce48c..84a6fca 100644
|
||||
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
|
||||
{
|
||||
[super insertReactSubview:view atIndex:atIndex];
|
||||
@@ -443,6 +430,8 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
|
||||
@@ -449,6 +436,8 @@ static inline void RCTApplyTransformationAccordingLayoutDirection(
|
||||
_contentView = view;
|
||||
RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
|
||||
[_scrollView addSubview:view];
|
||||
@@ -273,7 +273,7 @@ index e9ce48c..84a6fca 100644
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,9 +641,46 @@ -(void)delegateMethod : (UIScrollView *)scrollView \
|
||||
@@ -658,9 +647,46 @@ static inline void RCTApplyTransformationAccordingLayoutDirection(
|
||||
}
|
||||
|
||||
RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin)
|
||||
@@ -321,7 +321,7 @@ index e9ce48c..84a6fca 100644
|
||||
- (void)addScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
|
||||
{
|
||||
[_scrollListeners addObject:scrollListener];
|
||||
@@ -933,6 +959,7 @@ - (void)updateContentSizeIfNeeded
|
||||
@@ -939,6 +965,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
|
||||
CGSize contentSize = self.contentSize;
|
||||
if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
|
||||
_scrollView.contentSize = contentSize;
|
||||
@@ -329,7 +329,7 @@ index e9ce48c..84a6fca 100644
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1055,6 +1082,22 @@ -(type)getter \
|
||||
@@ -1061,6 +1088,22 @@ RCT_SET_AND_PRESERVE_OFFSET(setShowsHorizontalScrollIndicator, showsHorizontalSc
|
||||
RCT_SET_AND_PRESERVE_OFFSET(setShowsVerticalScrollIndicator, showsVerticalScrollIndicator, BOOL)
|
||||
RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat);
|
||||
|
||||
@@ -356,7 +356,7 @@ diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManag
|
||||
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
|
||||
@@ -83,6 +83,7 @@ RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval)
|
||||
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
||||
@@ -1,20 +1,20 @@
|
||||
import React, {useImperativeHandle} from 'react'
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
type NativeScrollEvent,
|
||||
type NativeSyntheticEvent,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleProp,
|
||||
type ScrollView,
|
||||
type StyleProp,
|
||||
TextInput,
|
||||
View,
|
||||
ViewStyle,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
KeyboardAwareScrollView,
|
||||
useKeyboardHandler,
|
||||
} from 'react-native-keyboard-controller'
|
||||
import {runOnJS} from 'react-native-reanimated'
|
||||
import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
|
||||
import {type ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -25,22 +25,22 @@ import {logger} from '#/logger'
|
||||
import {isAndroid, isIOS} from '#/platform/detection'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
||||
import {List, type ListMethods, type ListProps} from '#/view/com/util/List'
|
||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||
import {useThemeName} from '#/alf/util/useColorModeTheme'
|
||||
import {Context, useDialogContext} from '#/components/Dialog/context'
|
||||
import {
|
||||
DialogControlProps,
|
||||
DialogInnerProps,
|
||||
DialogOuterProps,
|
||||
type DialogControlProps,
|
||||
type DialogInnerProps,
|
||||
type DialogOuterProps,
|
||||
} from '#/components/Dialog/types'
|
||||
import {createInput} from '#/components/forms/TextField'
|
||||
import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
|
||||
import {
|
||||
BottomSheetSnapPointChangeEvent,
|
||||
BottomSheetStateChangeEvent,
|
||||
type BottomSheetSnapPointChangeEvent,
|
||||
type BottomSheetStateChangeEvent,
|
||||
} from '../../../modules/bottom-sheet/src/BottomSheet.types'
|
||||
import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet/src/BottomSheetNativeComponent'
|
||||
import {type BottomSheetNativeComponent} from '../../../modules/bottom-sheet/src/BottomSheetNativeComponent'
|
||||
|
||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||
export * from '#/components/Dialog/shared'
|
||||
@@ -299,7 +299,7 @@ export const InnerFlatList = React.forwardRef<
|
||||
keyboardShouldPersistTaps="handled"
|
||||
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
|
||||
ListFooterComponent={
|
||||
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
|
||||
<View style={{height: insets.bottom + a.pt_5xl.paddingTop + 50}} />
|
||||
}
|
||||
ref={ref}
|
||||
{...props}
|
||||
|
||||
@@ -7,24 +7,17 @@ import {
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import Animated, {
|
||||
LayoutAnimationConfig,
|
||||
LinearTransition,
|
||||
ZoomInEasyDown,
|
||||
} from 'react-native-reanimated'
|
||||
import {type AppBskyActorDefs, type ModerationOpts} from '@atproto/api'
|
||||
import {type ModerationOpts} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useActorSearchPaginated} from '#/state/queries/actor-search'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import {useSession} from '#/state/session'
|
||||
import {type Follow10ProgressGuide} from '#/state/shell/progress-guide'
|
||||
import {type ListMethods} from '#/view/com/util/List'
|
||||
@@ -49,15 +42,14 @@ import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/componen
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ListFooter} from '../Lists'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {ProgressGuideTask} from './Task'
|
||||
|
||||
type Item =
|
||||
| {
|
||||
type: 'profile'
|
||||
key: string
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
isSuggestion: boolean
|
||||
profile: bsky.profile.AnyProfileView
|
||||
}
|
||||
| {
|
||||
type: 'empty'
|
||||
@@ -128,94 +120,39 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
|
||||
const inputRef = useRef<TextInput>(null)
|
||||
const [headerHeight, setHeaderHeight] = useState(0)
|
||||
const {currentAccount} = useSession()
|
||||
const [suggestedAccounts, setSuggestedAccounts] = useState<
|
||||
Map<string, AppBskyActorDefs.ProfileView[]>
|
||||
>(() => new Map())
|
||||
|
||||
useEffect(() => {
|
||||
lastSearchText = searchText
|
||||
lastSelectedInterest = selectedInterest
|
||||
}, [searchText, selectedInterest])
|
||||
|
||||
const query = searchText || selectedInterest
|
||||
const {
|
||||
data: suggestions,
|
||||
isFetching: isFetchingSuggestions,
|
||||
error: suggestionsError,
|
||||
} = useGetSuggestedUsersQuery({
|
||||
category: selectedInterest,
|
||||
limit: 50,
|
||||
})
|
||||
const {
|
||||
data: searchResults,
|
||||
isFetching,
|
||||
error,
|
||||
isError,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
isFetching: isFetchingSearchResults,
|
||||
error: searchResultsError,
|
||||
isError: isSearchResultsError,
|
||||
} = useActorSearchPaginated({
|
||||
query,
|
||||
enabled: !!searchText,
|
||||
query: searchText,
|
||||
})
|
||||
|
||||
const hasSearchText = !!searchText
|
||||
|
||||
const resultsKey = searchText || selectedInterest
|
||||
const items = useMemo(() => {
|
||||
const results = searchResults?.pages.flatMap(r => r.actors)
|
||||
const results = hasSearchText
|
||||
? searchResults?.pages.flatMap(p => p.actors)
|
||||
: suggestions?.actors
|
||||
let _items: Item[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
if (isError) {
|
||||
_items.push({
|
||||
type: 'empty',
|
||||
key: 'empty',
|
||||
message: _(msg`We're having network issues, try again`),
|
||||
})
|
||||
} else if (results) {
|
||||
// First pass: search results
|
||||
for (const profile of results) {
|
||||
if (profile.did === currentAccount?.did) continue
|
||||
if (profile.viewer?.following) continue
|
||||
// my sincere apologies to Jake Gold - your bio is too keyword-filled and
|
||||
// your page-rank too high, so you're at the top of half the categories -sfn
|
||||
if (
|
||||
!hasSearchText &&
|
||||
profile.did === 'did:plc:tpg43qhh4lw4ksiffs4nbda3' &&
|
||||
// constrain to 'tech'
|
||||
selectedInterest !== 'tech'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
seen.add(profile.did)
|
||||
_items.push({
|
||||
type: 'profile',
|
||||
// Don't share identity across tabs or typing attempts
|
||||
key: query + ':' + profile.did,
|
||||
profile,
|
||||
isSuggestion: false,
|
||||
})
|
||||
}
|
||||
// Second pass: suggestions
|
||||
_items = _items.flatMap(item => {
|
||||
if (item.type !== 'profile') {
|
||||
return item
|
||||
}
|
||||
const suggestions = suggestedAccounts.get(item.profile.did)
|
||||
if (!suggestions) {
|
||||
return item
|
||||
}
|
||||
const itemWithSuggestions = [item]
|
||||
for (const suggested of suggestions) {
|
||||
if (seen.has(suggested.did)) {
|
||||
// Skip search results from previous step or already seen suggestions
|
||||
continue
|
||||
}
|
||||
seen.add(suggested.did)
|
||||
itemWithSuggestions.push({
|
||||
type: 'profile',
|
||||
key: suggested.did,
|
||||
profile: suggested,
|
||||
isSuggestion: true,
|
||||
})
|
||||
if (itemWithSuggestions.length === 1 + 3) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return itemWithSuggestions
|
||||
})
|
||||
} else {
|
||||
if (isFetchingSuggestions || isFetchingSearchResults) {
|
||||
const placeholders: Item[] = Array(10)
|
||||
.fill(0)
|
||||
.map((__, i) => ({
|
||||
@@ -224,21 +161,54 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
|
||||
}))
|
||||
|
||||
_items.push(...placeholders)
|
||||
} else if (
|
||||
(hasSearchText && searchResultsError) ||
|
||||
(!hasSearchText && suggestionsError) ||
|
||||
!results?.length
|
||||
) {
|
||||
_items.push({
|
||||
type: 'empty',
|
||||
key: 'empty',
|
||||
message: _(msg`We're having network issues, try again`),
|
||||
})
|
||||
} else {
|
||||
const seen = new Set<string>()
|
||||
for (const profile of results) {
|
||||
if (seen.has(profile.did)) continue
|
||||
if (profile.did === currentAccount?.did) continue
|
||||
if (profile.viewer?.following) continue
|
||||
|
||||
seen.add(profile.did)
|
||||
|
||||
_items.push({
|
||||
type: 'profile',
|
||||
// Don't share identity across tabs or typing attempts
|
||||
key: resultsKey + ':' + profile.did,
|
||||
profile,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return _items
|
||||
}, [
|
||||
_,
|
||||
suggestions,
|
||||
suggestionsError,
|
||||
isFetchingSuggestions,
|
||||
searchResults,
|
||||
isError,
|
||||
searchResultsError,
|
||||
isFetchingSearchResults,
|
||||
currentAccount?.did,
|
||||
hasSearchText,
|
||||
selectedInterest,
|
||||
suggestedAccounts,
|
||||
query,
|
||||
resultsKey,
|
||||
])
|
||||
|
||||
if (searchText && !isFetching && !items.length && !isError) {
|
||||
if (
|
||||
searchText &&
|
||||
!isFetchingSearchResults &&
|
||||
!items.length &&
|
||||
!isSearchResultsError
|
||||
) {
|
||||
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
|
||||
}
|
||||
|
||||
@@ -249,9 +219,7 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
|
||||
return (
|
||||
<FollowProfileCard
|
||||
profile={item.profile}
|
||||
isSuggestion={item.isSuggestion}
|
||||
moderationOpts={moderationOpts!}
|
||||
setSuggestedAccounts={setSuggestedAccounts}
|
||||
noBorder={index === 0}
|
||||
/>
|
||||
)
|
||||
@@ -297,15 +265,6 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
|
||||
/>
|
||||
)
|
||||
|
||||
const onEndReached = useCallback(async () => {
|
||||
if (isFetchingNextPage || !hasNextPage || isError) return
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more people to follow', {message: err})
|
||||
}
|
||||
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
return (
|
||||
<Dialog.InnerFlatList
|
||||
ref={listRef}
|
||||
@@ -325,15 +284,6 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
|
||||
scrollIndicatorInsets={{top: headerHeight}}
|
||||
initialNumToRender={8}
|
||||
maxToRenderPerBatch={8}
|
||||
onEndReached={onEndReached}
|
||||
itemLayoutAnimation={LinearTransition}
|
||||
ListFooterComponent={
|
||||
<ListFooter
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
error={cleanError(error)}
|
||||
onRetry={fetchNextPage}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -471,7 +421,6 @@ let Tabs = ({
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
}): React.ReactNode => {
|
||||
const listRef = useRef<ScrollView>(null)
|
||||
const [scrollX, setScrollX] = useState(0)
|
||||
const [totalWidth, setTotalWidth] = useState(0)
|
||||
const pendingTabOffsets = useRef<{x: number; width: number}[]>([])
|
||||
const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([])
|
||||
@@ -490,24 +439,11 @@ let Tabs = ({
|
||||
function scrollIntoViewIfNeeded(index: number) {
|
||||
const btnLayout = tabOffsets[index]
|
||||
if (!btnLayout) return
|
||||
|
||||
const viewportLeftEdge = scrollX
|
||||
const viewportRightEdge = scrollX + totalWidth
|
||||
const shouldScrollToLeftEdge = viewportLeftEdge > btnLayout.x
|
||||
const shouldScrollToRightEdge =
|
||||
viewportRightEdge < btnLayout.x + btnLayout.width
|
||||
|
||||
if (shouldScrollToLeftEdge) {
|
||||
listRef.current?.scrollTo({
|
||||
x: btnLayout.x - tokens.space.lg,
|
||||
animated: true,
|
||||
})
|
||||
} else if (shouldScrollToRightEdge) {
|
||||
listRef.current?.scrollTo({
|
||||
x: btnLayout.x - totalWidth + btnLayout.width + tokens.space.lg,
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
listRef.current?.scrollTo({
|
||||
// centered
|
||||
x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2),
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
|
||||
function handleSelectTab(index: number) {
|
||||
@@ -539,7 +475,7 @@ let Tabs = ({
|
||||
}
|
||||
onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
|
||||
scrollEventThrottle={200} // big throttle
|
||||
onScroll={evt => setScrollX(evt.nativeEvent.contentOffset.x)}>
|
||||
>
|
||||
{interests.map((interest, i) => {
|
||||
const active = interest === selectedInterest && !hasSearchText
|
||||
return (
|
||||
@@ -575,6 +511,7 @@ let Tab = ({
|
||||
interestsDisplayName: string
|
||||
onLayout: (index: number, x: number, width: number) => void
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const activeText = active ? _(msg` (active)`) : ''
|
||||
return (
|
||||
@@ -585,12 +522,32 @@ let Tab = ({
|
||||
}>
|
||||
<Button
|
||||
label={_(msg`Search for "${interestsDisplayName}"${activeText}`)}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
color={active ? 'primary' : 'secondary'}
|
||||
size="small"
|
||||
onPress={() => onSelectTab(index)}>
|
||||
<ButtonIcon icon={SearchIcon} />
|
||||
<ButtonText>{interestsDisplayName}</ButtonText>
|
||||
{({hovered, pressed, focused}) => (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.px_lg,
|
||||
a.py_sm,
|
||||
a.border,
|
||||
active || hovered || pressed || focused
|
||||
? [
|
||||
t.atoms.bg_contrast_25,
|
||||
{borderColor: t.atoms.bg_contrast_25.backgroundColor},
|
||||
]
|
||||
: [t.atoms.bg, t.atoms.border_contrast_low],
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
/* TODO: medium weight */
|
||||
active || hovered || pressed || focused
|
||||
? t.atoms.text
|
||||
: t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{interestsDisplayName}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
@@ -600,49 +557,18 @@ Tab = memo(Tab)
|
||||
let FollowProfileCard = ({
|
||||
profile,
|
||||
moderationOpts,
|
||||
isSuggestion,
|
||||
setSuggestedAccounts,
|
||||
noBorder,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
isSuggestion: boolean
|
||||
setSuggestedAccounts: (
|
||||
updater: (
|
||||
v: Map<string, AppBskyActorDefs.ProfileView[]>,
|
||||
) => Map<string, AppBskyActorDefs.ProfileView[]>,
|
||||
) => void
|
||||
noBorder?: boolean
|
||||
}): React.ReactNode => {
|
||||
const [hasFollowed, setHasFollowed] = useState(false)
|
||||
const followupSuggestion = useSuggestedFollowsByActorQuery({
|
||||
did: profile.did,
|
||||
enabled: hasFollowed,
|
||||
})
|
||||
const candidates = followupSuggestion.data?.suggestions
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Move out of effect.
|
||||
if (hasFollowed && candidates && candidates.length > 0) {
|
||||
setSuggestedAccounts(suggestions => {
|
||||
const newSuggestions = new Map(suggestions)
|
||||
newSuggestions.set(profile.did, candidates)
|
||||
return newSuggestions
|
||||
})
|
||||
}
|
||||
}, [hasFollowed, profile.did, candidates, setSuggestedAccounts])
|
||||
|
||||
return (
|
||||
<LayoutAnimationConfig skipEntering={!isSuggestion}>
|
||||
<Animated.View entering={native(ZoomInEasyDown)}>
|
||||
<FollowProfileCardInner
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
onFollow={() => setHasFollowed(true)}
|
||||
noBorder={noBorder}
|
||||
/>
|
||||
</Animated.View>
|
||||
</LayoutAnimationConfig>
|
||||
<FollowProfileCardInner
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
noBorder={noBorder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
FollowProfileCard = memo(FollowProfileCard)
|
||||
@@ -653,7 +579,7 @@ function FollowProfileCardInner({
|
||||
onFollow,
|
||||
noBorder,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
onFollow?: () => void
|
||||
noBorder?: boolean
|
||||
@@ -670,7 +596,7 @@ function FollowProfileCardInner({
|
||||
style={[
|
||||
a.flex_1,
|
||||
noBorder && a.border_t_0,
|
||||
(hovered || pressed) && t.atoms.border_contrast_high,
|
||||
(hovered || pressed) && t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<ProfileCard.Outer>
|
||||
<ProfileCard.Header>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function SubtleHover({style, hover}: ViewStyleProp & {hover: boolean}) {
|
||||
const t = useTheme()
|
||||
|
||||
let opacity: number
|
||||
switch (t.name) {
|
||||
case 'dark':
|
||||
opacity = 0.4
|
||||
break
|
||||
case 'dim':
|
||||
opacity = 0.45
|
||||
break
|
||||
case 'light':
|
||||
opacity = 0.5
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.pointer_events_none,
|
||||
a.transition_opacity,
|
||||
t.atoms.bg_contrast_25,
|
||||
style,
|
||||
{opacity: hover ? opacity : 0},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useConvoActive} from '#/state/messages/convo'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
@@ -61,6 +62,12 @@ export let MessageContextMenu = ({
|
||||
langPrefs.primaryLanguage,
|
||||
)
|
||||
openLink(translatorUrl, true)
|
||||
|
||||
logger.metric('translate', {
|
||||
sourceLanguages: [],
|
||||
targetLanguage: langPrefs.primaryLanguage,
|
||||
textLength: message.text.length,
|
||||
})
|
||||
}, [langPrefs.primaryLanguage, message.text, openLink])
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
|
||||
@@ -41,25 +41,27 @@ export function HostingProvider({
|
||||
onSelect={onSelectServiceUrl}
|
||||
/>
|
||||
{minimal ? (
|
||||
<View style={[a.flex_row, a.align_center, a.flex_wrap]}>
|
||||
<View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_xs]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
You are creating an account on{' '}
|
||||
<Button
|
||||
label={toNiceDomain(serviceUrl)}
|
||||
accessibilityHint={_(msg`Changes hosting provider`)}
|
||||
onPress={onPressSelectService}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="tiny"
|
||||
style={[a.px_xs, {margin: tokens.space.xs * -1}]}>
|
||||
<ButtonText style={[a.text_sm]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={PencilIcon} />
|
||||
</Button>
|
||||
</Trans>
|
||||
<Trans>You are creating an account on</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={toNiceDomain(serviceUrl)}
|
||||
accessibilityHint={_(msg`Changes hosting provider`)}
|
||||
onPress={onPressSelectService}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="tiny"
|
||||
style={[
|
||||
a.px_xs,
|
||||
{marginHorizontal: tokens.space.xs * -1},
|
||||
{paddingVertical: 0},
|
||||
]}>
|
||||
<ButtonText style={[a.text_sm]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={PencilIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
+858
-628
File diff suppressed because it is too large
Load Diff
+855
-625
File diff suppressed because it is too large
Load Diff
+856
-626
File diff suppressed because it is too large
Load Diff
+859
-629
File diff suppressed because it is too large
Load Diff
+858
-628
File diff suppressed because it is too large
Load Diff
+1127
-897
File diff suppressed because it is too large
Load Diff
+859
-629
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+330
-265
File diff suppressed because it is too large
Load Diff
+1137
-907
File diff suppressed because it is too large
Load Diff
+912
-682
File diff suppressed because it is too large
Load Diff
+857
-627
File diff suppressed because it is too large
Load Diff
+855
-625
File diff suppressed because it is too large
Load Diff
+857
-627
File diff suppressed because it is too large
Load Diff
+858
-628
File diff suppressed because it is too large
Load Diff
+862
-632
File diff suppressed because it is too large
Load Diff
+859
-629
File diff suppressed because it is too large
Load Diff
+855
-625
File diff suppressed because it is too large
Load Diff
+857
-627
File diff suppressed because it is too large
Load Diff
+1179
-949
File diff suppressed because it is too large
Load Diff
+857
-627
File diff suppressed because it is too large
Load Diff
+856
-626
File diff suppressed because it is too large
Load Diff
+861
-631
File diff suppressed because it is too large
Load Diff
+856
-626
File diff suppressed because it is too large
Load Diff
+860
-630
File diff suppressed because it is too large
Load Diff
+855
-625
File diff suppressed because it is too large
Load Diff
+856
-626
File diff suppressed because it is too large
Load Diff
+857
-627
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+856
-626
File diff suppressed because it is too large
Load Diff
+858
-628
File diff suppressed because it is too large
Load Diff
+856
-626
File diff suppressed because it is too large
Load Diff
+853
-623
File diff suppressed because it is too large
Load Diff
+940
-710
File diff suppressed because it is too large
Load Diff
+857
-627
File diff suppressed because it is too large
Load Diff
+859
-629
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -330,7 +330,6 @@ export type MetricEvents = {
|
||||
| 'suggestedFeeds'
|
||||
| 'suggestedStarterPacks'
|
||||
| `feed:${FeedDescriptor}`
|
||||
index: number
|
||||
}
|
||||
'explore:module:searchButtonPress': {
|
||||
module: 'suggestedAccounts' | 'suggestedFeeds'
|
||||
@@ -365,4 +364,10 @@ export type MetricEvents = {
|
||||
details: boolean
|
||||
}
|
||||
'reportDialog:failure': {}
|
||||
|
||||
translate: {
|
||||
sourceLanguages: string[]
|
||||
targetLanguage: string
|
||||
textLength: number
|
||||
}
|
||||
}
|
||||
|
||||
+222
-111
@@ -1,5 +1,5 @@
|
||||
import {useCallback, useMemo, useRef, useState} from 'react'
|
||||
import {View, type ViewabilityConfig, type ViewToken} from 'react-native'
|
||||
import {View, type ViewabilityConfig} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyFeedDefs,
|
||||
@@ -7,14 +7,17 @@ import {
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import * as bcp47Match from 'bcp-47-match'
|
||||
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {type MetricEvents} from '#/logger/metrics'
|
||||
import {useLanguagePrefs} from '#/state/preferences/languages'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {RQKEY_ROOT_PAGINATED as useActorSearchPaginatedQueryKeyRoot} from '#/state/queries/actor-search'
|
||||
import {
|
||||
type FeedPreviewItem,
|
||||
useFeedPreviews,
|
||||
@@ -22,9 +25,16 @@ import {
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {Nux, useNux} from '#/state/queries/nuxs'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
|
||||
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
|
||||
import {
|
||||
createGetSuggestedFeedsQueryKey,
|
||||
useGetSuggestedFeedsQuery,
|
||||
} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
|
||||
import {getSuggestedUsersQueryKeyRoot} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import {createGetTrendsQueryKey} from '#/state/queries/trending/useGetTrendsQuery'
|
||||
import {
|
||||
createSuggestedStarterPacksQueryKey,
|
||||
useSuggestedStarterPacksQuery,
|
||||
} from '#/state/queries/useSuggestedStarterPacksQuery'
|
||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||
import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
|
||||
import {PostFeedItem} from '#/view/com/posts/PostFeedItem'
|
||||
@@ -32,6 +42,10 @@ import {ViewFullThread} from '#/view/com/posts/ViewFullThread'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
||||
import {
|
||||
popularInterests,
|
||||
useInterestsDisplayNames,
|
||||
} from '#/screens/Onboarding/state'
|
||||
import {
|
||||
StarterPackCard,
|
||||
StarterPackCardSkeleton,
|
||||
@@ -40,7 +54,9 @@ import {ExploreInterestsCard} from '#/screens/Search/modules/ExploreInterestsCar
|
||||
import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations'
|
||||
import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
|
||||
import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
|
||||
import {atoms as a, native, platform, useTheme, web} from '#/alf'
|
||||
import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers'
|
||||
import {atoms as a, native, platform, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button} from '#/components/Button'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
|
||||
@@ -49,9 +65,12 @@ import {type Props as IcoProps} from '#/components/icons/common'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
|
||||
import {StarterPack} from '#/components/icons/StarterPack'
|
||||
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
|
||||
import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {boostInterests} from '#/components/ProgressGuide/FollowDialog'
|
||||
import {SubtleHover} from '#/components/SubtleHover'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as ModuleHeader from './components/ModuleHeader'
|
||||
import {
|
||||
@@ -69,33 +88,26 @@ function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
|
||||
onPress={item.onLoadMore}
|
||||
style={[a.relative, a.w_full]}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.px_lg,
|
||||
a.py_md,
|
||||
a.gap_sm,
|
||||
(hovered || pressed) && t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Text
|
||||
<>
|
||||
<SubtleHover hover={hovered || pressed} />
|
||||
<View
|
||||
style={[
|
||||
a.leading_snug,
|
||||
hovered ? t.atoms.text : t.atoms.text_contrast_medium,
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.px_lg,
|
||||
a.py_md,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
{item.message}
|
||||
</Text>
|
||||
{item.isLoadingMore ? (
|
||||
<Loader size="sm" />
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
size="sm"
|
||||
style={hovered ? t.atoms.text : t.atoms.text_contrast_medium}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[a.leading_snug]}>{item.message}</Text>
|
||||
{item.isLoadingMore ? (
|
||||
<Loader size="sm" />
|
||||
) : (
|
||||
<ChevronDownIcon size="sm" style={t.atoms.text_contrast_medium} />
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
@@ -112,6 +124,7 @@ type ExploreScreenItems =
|
||||
title: string
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
iconSize?: IcoProps['size']
|
||||
bottomBorder?: boolean
|
||||
searchButton?: {
|
||||
label: string
|
||||
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
|
||||
@@ -128,6 +141,7 @@ type ExploreScreenItems =
|
||||
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
|
||||
tab: 'user' | 'profile' | 'feed'
|
||||
}
|
||||
hideDefaultTab?: boolean
|
||||
}
|
||||
| {
|
||||
type: 'trendingTopics'
|
||||
@@ -144,9 +158,13 @@ type ExploreScreenItems =
|
||||
| {
|
||||
type: 'profile'
|
||||
key: string
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
recId?: number
|
||||
}
|
||||
| {
|
||||
type: 'profileEmpty'
|
||||
key: 'profileEmpty'
|
||||
}
|
||||
| {
|
||||
type: 'feed'
|
||||
key: string
|
||||
@@ -190,27 +208,42 @@ type ExploreScreenItems =
|
||||
|
||||
export function Explore({
|
||||
focusSearchInput,
|
||||
headerHeight,
|
||||
}: {
|
||||
focusSearchInput: (tab: 'user' | 'profile' | 'feed') => void
|
||||
headerHeight: number
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
const {data: preferences, error: preferencesError} = usePreferencesQuery()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const gate = useGate()
|
||||
const guide = useProgressGuide('follow-10')
|
||||
const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
|
||||
// TODO always get at least 10 back
|
||||
|
||||
/*
|
||||
* Begin special language handling
|
||||
*/
|
||||
const {contentLanguages} = useLanguagePrefs()
|
||||
const useFullExperience = useMemo(() => {
|
||||
if (contentLanguages.length === 0) return true
|
||||
return bcp47Match.basicFilter('en', contentLanguages).length > 0
|
||||
}, [contentLanguages])
|
||||
const personalizedInterests = preferences?.interests?.tags
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
const interests = Object.keys(interestsDisplayNames)
|
||||
.sort(boostInterests(popularInterests))
|
||||
.sort(boostInterests(personalizedInterests))
|
||||
const {
|
||||
data: suggestedUsers,
|
||||
isLoading: suggestedUsersIsLoading,
|
||||
error: suggestedUsersError,
|
||||
} = useGetSuggestedUsersQuery({
|
||||
category: selectedInterest,
|
||||
isRefetching: suggestedUsersIsRefetching,
|
||||
} = useSuggestedUsers({
|
||||
category: selectedInterest || (useFullExperience ? null : interests[0]),
|
||||
search: !useFullExperience,
|
||||
})
|
||||
/* End special language handling */
|
||||
|
||||
const {
|
||||
data: feeds,
|
||||
hasNextPage: hasNextFeedsPage,
|
||||
@@ -218,7 +251,7 @@ export function Explore({
|
||||
isFetchingNextPage: isFetchingNextFeedsPage,
|
||||
error: feedsError,
|
||||
fetchNextPage: fetchNextFeedsPage,
|
||||
} = useGetPopularFeedsQuery({limit: 10})
|
||||
} = useGetPopularFeedsQuery({limit: 10, enabled: useFullExperience})
|
||||
const interestsNux = useNux(Nux.ExploreInterestsCard)
|
||||
const showInterestsNux =
|
||||
interestsNux.status === 'ready' && !interestsNux.nux?.completed
|
||||
@@ -227,7 +260,8 @@ export function Explore({
|
||||
data: suggestedSPs,
|
||||
isLoading: isLoadingSuggestedSPs,
|
||||
error: suggestedSPsError,
|
||||
} = useSuggestedStarterPacksQuery()
|
||||
isRefetching: isRefetchingSuggestedSPs,
|
||||
} = useSuggestedStarterPacksQuery({enabled: useFullExperience})
|
||||
|
||||
const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
|
||||
const [hasPressedLoadMoreFeeds, setHasPressedLoadMoreFeeds] = useState(false)
|
||||
@@ -250,7 +284,9 @@ export function Explore({
|
||||
hasPressedLoadMoreFeeds,
|
||||
])
|
||||
|
||||
const {data: suggestedFeeds} = useGetSuggestedFeedsQuery()
|
||||
const {data: suggestedFeeds} = useGetSuggestedFeedsQuery({
|
||||
enabled: useFullExperience,
|
||||
})
|
||||
const {
|
||||
data: feedPreviewSlices,
|
||||
query: {
|
||||
@@ -260,7 +296,31 @@ export function Explore({
|
||||
hasNextPage: hasNextPageFeedPreviews,
|
||||
error: feedPreviewSlicesError,
|
||||
},
|
||||
} = useFeedPreviews(suggestedFeeds?.feeds ?? [])
|
||||
} = useFeedPreviews(suggestedFeeds?.feeds ?? [], useFullExperience)
|
||||
|
||||
const qc = useQueryClient()
|
||||
const [isPTR, setIsPTR] = useState(false)
|
||||
const onPTR = useCallback(async () => {
|
||||
setIsPTR(true)
|
||||
await Promise.all([
|
||||
await qc.resetQueries({
|
||||
queryKey: createGetTrendsQueryKey(),
|
||||
}),
|
||||
await qc.resetQueries({
|
||||
queryKey: createSuggestedStarterPacksQueryKey(),
|
||||
}),
|
||||
await qc.resetQueries({
|
||||
queryKey: [getSuggestedUsersQueryKeyRoot],
|
||||
}),
|
||||
await qc.resetQueries({
|
||||
queryKey: [useActorSearchPaginatedQueryKeyRoot],
|
||||
}),
|
||||
await qc.resetQueries({
|
||||
queryKey: createGetSuggestedFeedsQueryKey(),
|
||||
}),
|
||||
])
|
||||
setIsPTR(false)
|
||||
}, [qc, setIsPTR])
|
||||
|
||||
const onLoadMoreFeedPreviews = useCallback(async () => {
|
||||
if (
|
||||
@@ -303,9 +363,10 @@ export function Explore({
|
||||
metricsTag: 'suggestedAccounts',
|
||||
tab: 'user',
|
||||
},
|
||||
hideDefaultTab: !useFullExperience,
|
||||
})
|
||||
|
||||
if (suggestedUsersIsLoading) {
|
||||
if (suggestedUsersIsLoading || suggestedUsersIsRefetching) {
|
||||
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
||||
} else if (suggestedUsersError) {
|
||||
i.push({
|
||||
@@ -322,6 +383,7 @@ export function Explore({
|
||||
let seen = new Set()
|
||||
const profileItems: ExploreScreenItems[] = []
|
||||
for (const actor of suggestedUsers.actors) {
|
||||
// checking for following still necessary if search data is used
|
||||
if (!seen.has(actor.did) && !actor.viewer?.following) {
|
||||
seen.add(actor.did)
|
||||
profileItems.push({
|
||||
@@ -333,14 +395,23 @@ export function Explore({
|
||||
}
|
||||
|
||||
if (profileItems.length === 0) {
|
||||
// no items! remove the header
|
||||
i.pop()
|
||||
i.push({
|
||||
type: 'profileEmpty',
|
||||
key: 'profileEmpty',
|
||||
})
|
||||
} else {
|
||||
i.push(...profileItems)
|
||||
if (selectedInterest === null && useFullExperience) {
|
||||
// First "For You" tab, only show 5 to keep screen short
|
||||
i.push(...profileItems.slice(0, 5))
|
||||
} else {
|
||||
i.push(...profileItems)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no items! remove the header
|
||||
i.pop()
|
||||
i.push({
|
||||
type: 'profileEmpty',
|
||||
key: 'profileEmpty',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
||||
@@ -352,7 +423,10 @@ export function Explore({
|
||||
moderationOpts,
|
||||
suggestedUsers,
|
||||
suggestedUsersIsLoading,
|
||||
suggestedUsersIsRefetching,
|
||||
suggestedUsersError,
|
||||
selectedInterest,
|
||||
useFullExperience,
|
||||
])
|
||||
const suggestedFeedsModule = useMemo(() => {
|
||||
const i: ExploreScreenItems[] = []
|
||||
@@ -466,7 +540,7 @@ export function Explore({
|
||||
iconSize: 'xl',
|
||||
})
|
||||
|
||||
if (isLoadingSuggestedSPs) {
|
||||
if (isLoadingSuggestedSPs || isRefetchingSuggestedSPs) {
|
||||
Array.from({length: 3}).forEach((__, index) =>
|
||||
i.push({
|
||||
type: 'starterPackSkeleton',
|
||||
@@ -486,7 +560,13 @@ export function Explore({
|
||||
})
|
||||
}
|
||||
return i
|
||||
}, [suggestedSPs, _, isLoadingSuggestedSPs, suggestedSPsError])
|
||||
}, [
|
||||
suggestedSPs,
|
||||
_,
|
||||
isLoadingSuggestedSPs,
|
||||
suggestedSPsError,
|
||||
isRefetchingSuggestedSPs,
|
||||
])
|
||||
const feedPreviewsModule = useMemo(() => {
|
||||
const i: ExploreScreenItems[] = []
|
||||
i.push(...feedPreviewSlices)
|
||||
@@ -517,22 +597,35 @@ export function Explore({
|
||||
|
||||
i.push(topBorder)
|
||||
i.push(...interestsNuxModule)
|
||||
if (isNewUser) {
|
||||
i.push(...suggestedFollowsModule)
|
||||
i.push(...suggestedStarterPacksModule)
|
||||
i.push(trendingTopicsModule)
|
||||
|
||||
if (useFullExperience) {
|
||||
if (isNewUser) {
|
||||
i.push(...suggestedFollowsModule)
|
||||
i.push(...suggestedStarterPacksModule)
|
||||
i.push({
|
||||
type: 'header',
|
||||
key: 'trending-topics-header',
|
||||
title: _(msg`Trending topics`),
|
||||
icon: Graph,
|
||||
bottomBorder: true,
|
||||
})
|
||||
i.push(trendingTopicsModule)
|
||||
} else {
|
||||
i.push(trendingTopicsModule)
|
||||
i.push(...suggestedFollowsModule)
|
||||
i.push(...suggestedStarterPacksModule)
|
||||
}
|
||||
if (gate('explore_show_suggested_feeds')) {
|
||||
i.push(...suggestedFeedsModule)
|
||||
}
|
||||
i.push(...feedPreviewsModule)
|
||||
} else {
|
||||
i.push(trendingTopicsModule)
|
||||
i.push(...suggestedFollowsModule)
|
||||
i.push(...suggestedStarterPacksModule)
|
||||
}
|
||||
if (gate('explore_show_suggested_feeds')) {
|
||||
i.push(...suggestedFeedsModule)
|
||||
}
|
||||
i.push(...feedPreviewsModule)
|
||||
|
||||
return i
|
||||
}, [
|
||||
_,
|
||||
topBorder,
|
||||
isNewUser,
|
||||
suggestedFollowsModule,
|
||||
@@ -542,6 +635,7 @@ export function Explore({
|
||||
feedPreviewsModule,
|
||||
interestsNuxModule,
|
||||
gate,
|
||||
useFullExperience,
|
||||
])
|
||||
|
||||
const renderItem = useCallback(
|
||||
@@ -549,22 +643,11 @@ export function Explore({
|
||||
switch (item.type) {
|
||||
case 'topBorder':
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
t.atoms.border_contrast_low,
|
||||
a.border_t,
|
||||
headerHeight &&
|
||||
web({
|
||||
position: 'sticky',
|
||||
top: headerHeight,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
<View style={[a.w_full, t.atoms.border_contrast_low, a.border_t]} />
|
||||
)
|
||||
case 'header': {
|
||||
return (
|
||||
<ModuleHeader.Container>
|
||||
<ModuleHeader.Container bottomBorder={item.bottomBorder}>
|
||||
<ModuleHeader.Icon icon={item.icon} size={item.iconSize} />
|
||||
<ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText>
|
||||
{item.searchButton && (
|
||||
@@ -596,6 +679,7 @@ export function Explore({
|
||||
<SuggestedAccountsTabBar
|
||||
selectedInterest={selectedInterest}
|
||||
onSelectInterest={setSelectedInterest}
|
||||
hideDefaultTab={item.hideDefaultTab}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
@@ -623,6 +707,21 @@ export function Explore({
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'profileEmpty': {
|
||||
return (
|
||||
<View style={[a.px_lg, a.pb_lg]}>
|
||||
<Admonition>
|
||||
{selectedInterest ? (
|
||||
<Trans>
|
||||
No results for "{interestsDisplayNames[selectedInterest]}".
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>No results.</Trans>
|
||||
)}
|
||||
</Admonition>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
case 'feed': {
|
||||
return (
|
||||
<View
|
||||
@@ -737,8 +836,13 @@ export function Explore({
|
||||
case 'preview:header': {
|
||||
return (
|
||||
<ModuleHeader.Container
|
||||
headerHeight={headerHeight}
|
||||
style={[a.pt_xs, a.border_b, t.atoms.border_contrast_low]}>
|
||||
style={[
|
||||
a.pt_xs,
|
||||
t.atoms.border_contrast_low,
|
||||
native(a.border_b),
|
||||
]}>
|
||||
{/* Very non-scientific way to avoid small gap on scroll */}
|
||||
<View style={[a.absolute, a.inset_0, t.atoms.bg, {top: -2}]} />
|
||||
<ModuleHeader.FeedLink feed={item.feed}>
|
||||
<ModuleHeader.FeedAvatar feed={item.feed} />
|
||||
<View style={[a.flex_1, a.gap_xs]}>
|
||||
@@ -817,9 +921,9 @@ export function Explore({
|
||||
focusSearchInput,
|
||||
moderationOpts,
|
||||
selectedInterest,
|
||||
interestsDisplayNames,
|
||||
_,
|
||||
fetchNextPageFeedPreviews,
|
||||
headerHeight,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -837,36 +941,26 @@ export function Explore({
|
||||
|
||||
// track headers and report module viewability
|
||||
const alreadyReportedRef = useRef<Map<string, string>>(new Map())
|
||||
const onViewableItemsChanged = useCallback(
|
||||
({
|
||||
viewableItems,
|
||||
}: {
|
||||
viewableItems: ViewToken<ExploreScreenItems>[]
|
||||
changed: ViewToken<ExploreScreenItems>[]
|
||||
}) => {
|
||||
for (const {item, index} of viewableItems.filter(vi => vi.isViewable)) {
|
||||
let module: MetricEvents['explore:module:seen']['module']
|
||||
if (item.type === 'trendingTopics' || item.type === 'trendingVideos') {
|
||||
module = item.type
|
||||
} else if (item.type === 'profile') {
|
||||
module = 'suggestedAccounts'
|
||||
} else if (item.type === 'feed') {
|
||||
module = 'suggestedFeeds'
|
||||
} else if (item.type === 'starterPack') {
|
||||
module = 'suggestedStarterPacks'
|
||||
} else if (item.type === 'preview:header') {
|
||||
module = `feed:feedgen|${item.feed.uri}`
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if (!alreadyReportedRef.current.has(module)) {
|
||||
alreadyReportedRef.current.set(module, module)
|
||||
logger.metric('explore:module:seen', {module, index: index ?? -1})
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
const onItemSeen = useCallback((item: ExploreScreenItems) => {
|
||||
let module: MetricEvents['explore:module:seen']['module']
|
||||
if (item.type === 'trendingTopics' || item.type === 'trendingVideos') {
|
||||
module = item.type
|
||||
} else if (item.type === 'profile') {
|
||||
module = 'suggestedAccounts'
|
||||
} else if (item.type === 'feed') {
|
||||
module = 'suggestedFeeds'
|
||||
} else if (item.type === 'starterPack') {
|
||||
module = 'suggestedStarterPacks'
|
||||
} else if (item.type === 'preview:sliceItem') {
|
||||
module = `feed:feedgen|${item.feed.uri}`
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if (!alreadyReportedRef.current.has(module)) {
|
||||
alreadyReportedRef.current.set(module, module)
|
||||
logger.metric('explore:module:seen', {module})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<List
|
||||
@@ -879,13 +973,30 @@ export function Explore({
|
||||
keyboardDismissMode="on-drag"
|
||||
stickyHeaderIndices={native(stickyHeaderIndices)}
|
||||
viewabilityConfig={viewabilityConfig}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
onItemSeen={onItemSeen}
|
||||
onEndReached={onLoadMoreFeedPreviews}
|
||||
onEndReachedThreshold={3}
|
||||
initialNumToRender={initialNumToRender}
|
||||
windowSize={9}
|
||||
maxToRenderPerBatch={platform({ios: 5, default: 1})}
|
||||
updateCellsBatchingPeriod={40}
|
||||
/**
|
||||
* Default: 2
|
||||
*/
|
||||
onEndReachedThreshold={4}
|
||||
/**
|
||||
* Default: 10
|
||||
*/
|
||||
initialNumToRender={10}
|
||||
/**
|
||||
* Default: 21
|
||||
*/
|
||||
windowSize={platform({android: 11})}
|
||||
/**
|
||||
* Default: 10
|
||||
*/
|
||||
maxToRenderPerBatch={platform({android: 1})}
|
||||
/**
|
||||
* Default: 50
|
||||
*/
|
||||
updateCellsBatchingPeriod={platform({android: 25})}
|
||||
refreshing={isPTR}
|
||||
onRefresh={onPTR}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||
import {makeCustomFeedLink} from '#/lib/routes/links'
|
||||
import {logger} from '#/logger'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, native, useTheme, type ViewStyleProp, web} from '#/alf'
|
||||
import {atoms as a, native, useTheme, type ViewStyleProp} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {sizes as iconSizes} from '#/components/icons/common'
|
||||
@@ -17,8 +17,11 @@ import {Text, type TextProps} from '#/components/Typography'
|
||||
export function Container({
|
||||
style,
|
||||
children,
|
||||
headerHeight,
|
||||
}: {children: React.ReactNode; headerHeight?: number} & ViewStyleProp) {
|
||||
bottomBorder,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
bottomBorder?: boolean
|
||||
} & ViewStyleProp) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
@@ -30,11 +33,9 @@ export function Container({
|
||||
a.pb_md,
|
||||
a.gap_sm,
|
||||
t.atoms.bg,
|
||||
headerHeight && web({position: 'sticky', top: headerHeight}),
|
||||
bottomBorder && [a.border_b, t.atoms.border_contrast_low],
|
||||
style,
|
||||
]}>
|
||||
{/* Very non-scientific way to avoid small gap on scroll */}
|
||||
<View style={[a.absolute, a.inset_0, t.atoms.bg, {top: -2}]} />
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
|
||||
import {Link} from '#/components/Link'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {useStarterPackLink} from '#/components/StarterPack/StarterPackCard'
|
||||
import {SubtleHover} from '#/components/SubtleHover'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
@@ -32,10 +33,11 @@ export function StarterPackCard({
|
||||
const {currentAccount} = useSession()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const link = useStarterPackLink({view})
|
||||
const record = view.record
|
||||
|
||||
if (
|
||||
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||
view.record,
|
||||
record,
|
||||
AppBskyGraphStarterpack.isRecord,
|
||||
)
|
||||
) {
|
||||
@@ -48,75 +50,80 @@ export function StarterPackCard({
|
||||
.map(item => item.subject)
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.p_lg,
|
||||
a.gap_md,
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<View aria-hidden style={[a.absolute, a.inset_0, a.z_40]}>
|
||||
<Link
|
||||
to={link.to}
|
||||
label={link.label}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
onHoverIn={link.precache}
|
||||
onPress={link.precache}>
|
||||
<View />
|
||||
</Link>
|
||||
</View>
|
||||
<Link
|
||||
to={link.to}
|
||||
label={link.label}
|
||||
onHoverIn={link.precache}
|
||||
onPress={link.precache}>
|
||||
{s => (
|
||||
<>
|
||||
<SubtleHover hover={s.hovered || s.pressed} />
|
||||
|
||||
<AvatarStack
|
||||
profiles={profiles ?? []}
|
||||
numPending={profileCount}
|
||||
total={view.list?.listItemCount}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.p_lg,
|
||||
a.gap_md,
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<AvatarStack
|
||||
profiles={profiles ?? []}
|
||||
numPending={profileCount}
|
||||
total={view.list?.listItemCount}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.align_start,
|
||||
a.gap_lg,
|
||||
web({
|
||||
position: 'static',
|
||||
zIndex: 'unset',
|
||||
}),
|
||||
]}>
|
||||
<View style={[a.flex_1]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_md, a.font_bold, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
{view.record.name}
|
||||
</Text>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
{view.creator?.did === currentAccount?.did
|
||||
? _(msg`By you`)
|
||||
: _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)}
|
||||
</Text>
|
||||
</View>
|
||||
<Link
|
||||
to={link.to}
|
||||
label={link.label}
|
||||
onHoverIn={link.precache}
|
||||
onPress={link.precache}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="small"
|
||||
style={[a.z_50]}>
|
||||
<ButtonText>
|
||||
<Trans>Open pack</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.align_start,
|
||||
a.gap_lg,
|
||||
web({
|
||||
position: 'static',
|
||||
zIndex: 'unset',
|
||||
}),
|
||||
]}>
|
||||
<View style={[a.flex_1]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_md, a.font_bold, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
{record.name}
|
||||
</Text>
|
||||
<Text
|
||||
emoji
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{view.creator?.did === currentAccount?.did
|
||||
? _(msg`By you`)
|
||||
: _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)}
|
||||
</Text>
|
||||
</View>
|
||||
<Link
|
||||
to={link.to}
|
||||
label={link.label}
|
||||
onHoverIn={link.precache}
|
||||
onPress={link.precache}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="small"
|
||||
style={[a.z_50]}>
|
||||
<ButtonText>
|
||||
<Trans>Open pack</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Shapes_Stroke2_Corner0_Rounded as Shapes} from '#/components/icons/Shapes'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Link} from '#/components/Link'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
@@ -39,32 +40,33 @@ export function ExploreInterestsCard() {
|
||||
<>
|
||||
<Prompt.Basic
|
||||
control={trendingPrompt}
|
||||
title={_(msg`Your interests`)}
|
||||
title={_(msg`Dismiss interests`)}
|
||||
description={_(
|
||||
msg`You can adjust your interests at any time from your "Content and media" settings.`,
|
||||
msg`You can adjust your interests at any time from "Content and media" settings.`,
|
||||
)}
|
||||
confirmButtonCta={_(
|
||||
msg({
|
||||
message: `Copy that!`,
|
||||
comment: `Confirm button text. Can be a short cheeky phrase that means "OK" e.g. "Copy that!"`,
|
||||
message: `OK`,
|
||||
comment: `Confirm button text.`,
|
||||
}),
|
||||
)}
|
||||
onConfirm={onConfirmClose}
|
||||
/>
|
||||
|
||||
<View style={[a.p_lg, a.pb_2xs]}>
|
||||
<View style={[a.pb_2xs]}>
|
||||
<View
|
||||
style={[
|
||||
a.p_lg,
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
a.gap_sm,
|
||||
a.border_b,
|
||||
a.gap_md,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Text style={[a.text_md, a.font_bold, a.leading_tight]}>
|
||||
<Trans>Your interests</Trans>
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
|
||||
<Shapes />
|
||||
<Text style={[a.text_xl, a.font_bold, a.leading_tight]}>
|
||||
<Trans>Your interests</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{preferences?.interests?.tags &&
|
||||
preferences.interests.tags.length > 0 ? (
|
||||
@@ -76,12 +78,11 @@ export function ExploreInterestsCard() {
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.rounded_full,
|
||||
a.border,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.px_lg,
|
||||
{height: 32},
|
||||
]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_high]}>
|
||||
{interestsDisplayNames[tag]}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -89,10 +90,8 @@ export function ExploreInterestsCard() {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<Text style={[a.text_sm, a.leading_snug, a.pb_xs]}>
|
||||
<Trans>
|
||||
Your selected interests help us serve you content you care about.
|
||||
</Trans>
|
||||
<Text style={[a.text_sm, a.leading_snug]}>
|
||||
<Trans>Your interests help us find what you like!</Trans>
|
||||
</Text>
|
||||
|
||||
<Link
|
||||
@@ -110,15 +109,15 @@ export function ExploreInterestsCard() {
|
||||
<Button
|
||||
label={_(msg`Hide this card`)}
|
||||
size="small"
|
||||
variant="solid"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={onClose}
|
||||
style={[
|
||||
a.absolute,
|
||||
{top: a.pt_xs.paddingTop, right: a.pr_xs.paddingRight},
|
||||
{top: a.pt_sm.paddingTop, right: a.pr_sm.paddingRight},
|
||||
]}>
|
||||
<ButtonIcon icon={X} />
|
||||
<ButtonIcon icon={X} size="md" />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {atoms as a} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {boostInterests, Tabs} from '#/components/ProgressGuide/FollowDialog'
|
||||
import {SubtleHover} from '#/components/SubtleHover'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
@@ -57,9 +58,11 @@ export function useLoadEnoughProfiles({
|
||||
export function SuggestedAccountsTabBar({
|
||||
selectedInterest,
|
||||
onSelectInterest,
|
||||
hideDefaultTab,
|
||||
}: {
|
||||
selectedInterest: string | null
|
||||
onSelectInterest: (interest: string | null) => void
|
||||
hideDefaultTab?: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
@@ -71,8 +74,10 @@ export function SuggestedAccountsTabBar({
|
||||
return (
|
||||
<BlockDrawerGesture>
|
||||
<Tabs
|
||||
interests={['all', ...interests]}
|
||||
selectedInterest={selectedInterest || 'all'}
|
||||
interests={hideDefaultTab ? interests : ['all', ...interests]}
|
||||
selectedInterest={
|
||||
selectedInterest || (hideDefaultTab ? interests[0] : 'all')
|
||||
}
|
||||
onSelectTab={tab => {
|
||||
logger.metric(
|
||||
'explore:suggestedAccounts:tabPressed',
|
||||
@@ -82,10 +87,14 @@ export function SuggestedAccountsTabBar({
|
||||
onSelectInterest(tab === 'all' ? null : tab)
|
||||
}}
|
||||
hasSearchText={false}
|
||||
interestsDisplayNames={{
|
||||
all: _(msg`For You`),
|
||||
...interestsDisplayNames,
|
||||
}}
|
||||
interestsDisplayNames={
|
||||
hideDefaultTab
|
||||
? interestsDisplayNames
|
||||
: {
|
||||
all: _(msg`For You`),
|
||||
...interestsDisplayNames,
|
||||
}
|
||||
}
|
||||
TabComponent={Tab}
|
||||
contentContainerStyle={[
|
||||
{
|
||||
@@ -133,10 +142,7 @@ let Tab = ({
|
||||
a.py_sm,
|
||||
a.border,
|
||||
active || hovered || pressed || focused
|
||||
? [
|
||||
t.atoms.bg_contrast_25,
|
||||
{borderColor: t.atoms.bg_contrast_25.backgroundColor},
|
||||
]
|
||||
? [t.atoms.bg_contrast_25, t.atoms.border_contrast_medium]
|
||||
: [t.atoms.bg, t.atoms.border_contrast_low],
|
||||
]}>
|
||||
<Text
|
||||
@@ -186,47 +192,52 @@ let SuggestedProfileCard = ({
|
||||
{statsig: true},
|
||||
)
|
||||
}}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.py_lg,
|
||||
a.px_lg,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
a.flex_1,
|
||||
]}>
|
||||
<ProfileCard.Outer>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.FollowButton
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
withIcon={false}
|
||||
logContext="ExploreSuggestedAccounts"
|
||||
onFollow={() => {
|
||||
logger.metric(
|
||||
'suggestedUser:follow',
|
||||
{
|
||||
logContext: 'Explore',
|
||||
location: 'Card',
|
||||
recId,
|
||||
position,
|
||||
},
|
||||
{statsig: true},
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</ProfileCard.Header>
|
||||
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
||||
</ProfileCard.Outer>
|
||||
</View>
|
||||
{s => (
|
||||
<>
|
||||
<SubtleHover hover={s.hovered || s.pressed} />
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.w_full,
|
||||
a.py_lg,
|
||||
a.px_lg,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<ProfileCard.Outer>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.FollowButton
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
withIcon={false}
|
||||
logContext="ExploreSuggestedAccounts"
|
||||
onFollow={() => {
|
||||
logger.metric(
|
||||
'suggestedUser:follow',
|
||||
{
|
||||
logContext: 'Explore',
|
||||
location: 'Card',
|
||||
recId,
|
||||
position,
|
||||
},
|
||||
{statsig: true},
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</ProfileCard.Header>
|
||||
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
||||
</ProfileCard.Outer>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</ProfileCard.Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {Flame_Stroke2_Corner1_Rounded as FlameIcon} from '#/components/icons/Flame'
|
||||
import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
|
||||
import {Link} from '#/components/Link'
|
||||
import {SubtleHover} from '#/components/SubtleHover'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const TOPIC_COUNT = 5
|
||||
@@ -28,10 +29,10 @@ export function ExploreTrendingTopics() {
|
||||
}
|
||||
|
||||
function Inner() {
|
||||
const {data: trending, error, isLoading} = useGetTrendsQuery()
|
||||
const {data: trending, error, isLoading, isRefetching} = useGetTrendsQuery()
|
||||
const noTopics = !isLoading && !error && !trending?.trends?.length
|
||||
|
||||
return isLoading ? (
|
||||
return isLoading || isRefetching ? (
|
||||
Array.from({length: TOPIC_COUNT}).map((__, i) => (
|
||||
<TrendingTopicRowSkeleton key={i} withPosts={i === 0} />
|
||||
))
|
||||
@@ -92,25 +93,23 @@ export function TrendRow({
|
||||
PressableComponent={Pressable}>
|
||||
{({hovered, pressed}) => (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
gutters,
|
||||
a.w_full,
|
||||
a.py_lg,
|
||||
a.flex_row,
|
||||
a.gap_2xs,
|
||||
(hovered || pressed) && t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<SubtleHover hover={hovered || pressed} />
|
||||
<View style={[gutters, a.w_full, a.py_lg, a.flex_row, a.gap_2xs]}>
|
||||
<View style={[a.flex_1, a.gap_xs]}>
|
||||
<View style={[a.flex_row]}>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, a.leading_snug, {width: 20}]}>
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
{width: 20},
|
||||
]}>
|
||||
<Trans comment='The trending topic rank, i.e. "1. March Madness", "2. The Bachelor"'>
|
||||
{rank}.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, a.leading_snug]}
|
||||
style={[a.text_md, a.font_bold, a.leading_tight]}
|
||||
numberOfLines={1}>
|
||||
{trend.displayName}
|
||||
</Text>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {useMemo} from 'react'
|
||||
|
||||
import {useActorSearchPaginated} from '#/state/queries/actor-search'
|
||||
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
|
||||
|
||||
/**
|
||||
* Conditional hook, used in case a user is a non-english speaker, in which
|
||||
* case we fall back to searching for users instead of our more curated set.
|
||||
*/
|
||||
export function useSuggestedUsers({
|
||||
category = null,
|
||||
search = false,
|
||||
}: {
|
||||
category?: string | null
|
||||
/**
|
||||
* If true, we'll search for users using the translated value of `category`,
|
||||
* based on the user's "app language setting
|
||||
*/
|
||||
search?: boolean
|
||||
}) {
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
const curated = useGetSuggestedUsersQuery({
|
||||
enabled: !search,
|
||||
category,
|
||||
})
|
||||
const searched = useActorSearchPaginated({
|
||||
enabled: !!search,
|
||||
// use user's app language translation for this value
|
||||
query: category ? interestsDisplayNames[category] : '',
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
return useMemo(() => {
|
||||
if (search) {
|
||||
return {
|
||||
// we're not paginating right now
|
||||
data: searched?.data
|
||||
? {
|
||||
actors: searched.data.pages.flatMap(p => p.actors) ?? [],
|
||||
}
|
||||
: undefined,
|
||||
isLoading: searched.isLoading,
|
||||
error: searched.error,
|
||||
isRefetching: searched.isRefetching,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
data: curated.data,
|
||||
isLoading: curated.isLoading,
|
||||
error: curated.error,
|
||||
isRefetching: curated.isRefetching,
|
||||
}
|
||||
}
|
||||
}, [curated, searched, search])
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
usePreferencesQuery,
|
||||
} from '#/state/queries/preferences'
|
||||
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
import {createGetSuggestedFeedsQueryKey} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
|
||||
import {createGetSuggestedUsersQueryKey} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import {createSuggestedStarterPacksQueryKey} from '#/state/queries/useSuggestedStarterPacksQuery'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
|
||||
@@ -98,15 +101,37 @@ function Inner({
|
||||
|
||||
try {
|
||||
await agent.setInterestsPref({tags: interests})
|
||||
await qc.invalidateQueries({queryKey: preferencesQueryKey})
|
||||
qc.setQueriesData(
|
||||
{queryKey: preferencesQueryKey},
|
||||
(old?: UsePreferencesQueryResponse) => {
|
||||
if (!old) return old
|
||||
old.interests.tags = interests
|
||||
return old
|
||||
},
|
||||
)
|
||||
await Promise.all([
|
||||
await qc.resetQueries({
|
||||
queryKey: createSuggestedStarterPacksQueryKey(),
|
||||
}),
|
||||
await qc.resetQueries({queryKey: createGetSuggestedFeedsQueryKey()}),
|
||||
await qc.resetQueries({
|
||||
queryKey: createGetSuggestedUsersQueryKey({}),
|
||||
}),
|
||||
])
|
||||
|
||||
Toast.show(
|
||||
_(msg({message: 'Content preferences updated!', context: 'toast'})),
|
||||
_(
|
||||
msg({
|
||||
message: 'Your interests have been updated!',
|
||||
context: 'toast',
|
||||
}),
|
||||
),
|
||||
)
|
||||
} catch (error) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg({
|
||||
message: 'Failed to save content prefefences.',
|
||||
message: 'Failed to save your interests.',
|
||||
context: 'toast',
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -17,7 +17,7 @@ import {useAgent} from '#/state/session'
|
||||
const RQKEY_ROOT = 'actor-search'
|
||||
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
|
||||
|
||||
const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
|
||||
export const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
|
||||
export const RQKEY_PAGINATED = (query: string, limit?: number) => [
|
||||
RQKEY_ROOT_PAGINATED,
|
||||
query,
|
||||
|
||||
@@ -34,6 +34,41 @@ const RQKEY_ROOT = 'feed-previews'
|
||||
const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds]
|
||||
|
||||
const LIMIT = 8 // sliced to 6, overfetch to account for moderation
|
||||
const PINNED_POST_URIS: Record<string, boolean> = {
|
||||
// 📰 News
|
||||
'at://did:plc:kkf4naxqmweop7dv4l2iqqf5/app.bsky.feed.post/3lgh27w2ngc2b':
|
||||
true,
|
||||
// Gardening
|
||||
'at://did:plc:5rw2on4i56btlcajojaxwcat/app.bsky.feed.post/3kjorckgcwc27':
|
||||
true,
|
||||
// Web Development Trending
|
||||
'at://did:plc:m2sjv3wncvsasdapla35hzwj/app.bsky.feed.post/3lfaw445axs22':
|
||||
true,
|
||||
// Anime & Manga EN
|
||||
'at://did:plc:tazrmeme4dzahimsykusrwrk/app.bsky.feed.post/3knxx2gmkns2y':
|
||||
true,
|
||||
// 📽️ Film
|
||||
'at://did:plc:2hwwem55ce6djnk6bn62cstr/app.bsky.feed.post/3llhpzhbq7c2g':
|
||||
true,
|
||||
// PopSky
|
||||
'at://did:plc:lfdf4srj43iwdng7jn35tjsp/app.bsky.feed.post/3lbblgly65c2g':
|
||||
true,
|
||||
// Science
|
||||
'at://did:plc:hu2obebw3nhfj667522dahfg/app.bsky.feed.post/3kl33otd6ob2s':
|
||||
true,
|
||||
// Birds! 🦉
|
||||
'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.post/3lbg4r57yk22d':
|
||||
true,
|
||||
// Astronomy
|
||||
'at://did:plc:xy2zorw2ys47poflotxthlzg/app.bsky.feed.post/3kyzye4lujs2w':
|
||||
true,
|
||||
// What's Cooking 🍽️
|
||||
'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3lfqhgvxbqc2q':
|
||||
true,
|
||||
// BookSky 💙📚 #booksky
|
||||
'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3kgrm2rw5ww2e':
|
||||
true,
|
||||
}
|
||||
|
||||
export type FeedPreviewItem =
|
||||
| {
|
||||
@@ -73,6 +108,7 @@ export type FeedPreviewItem =
|
||||
key: string
|
||||
slice: FeedPostSlice
|
||||
indexInSlice: number
|
||||
feed: AppBskyFeedDefs.GeneratorView
|
||||
showReplyTo: boolean
|
||||
hideTopBorder: boolean
|
||||
}
|
||||
@@ -84,6 +120,7 @@ export type FeedPreviewItem =
|
||||
|
||||
export function useFeedPreviews(
|
||||
feedsMaybeWithDuplicates: AppBskyFeedDefs.GeneratorView[],
|
||||
isEnabled: boolean = true,
|
||||
) {
|
||||
const feeds = useMemo(
|
||||
() =>
|
||||
@@ -99,7 +136,7 @@ export function useFeedPreviews(
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const userInterests = aggregateUserInterests(preferences)
|
||||
const moderationOpts = useModerationOpts()
|
||||
const enabled = feeds.length > 0
|
||||
const enabled = feeds.length > 0 && isEnabled
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
enabled,
|
||||
@@ -180,19 +217,24 @@ export function useFeedPreviews(
|
||||
feedContext: item.feedContext,
|
||||
reason: item.reason,
|
||||
feedPostUri: item.feedPostUri,
|
||||
items: item.items.slice(0, 6).map((subItem, i) => {
|
||||
const feedPostSliceItem: FeedPostSliceItem = {
|
||||
_reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
|
||||
uri: subItem.post.uri,
|
||||
post: subItem.post,
|
||||
record: subItem.record,
|
||||
moderation: moderations[i],
|
||||
parentAuthor: subItem.parentAuthor,
|
||||
isParentBlocked: subItem.isParentBlocked,
|
||||
isParentNotFound: subItem.isParentNotFound,
|
||||
}
|
||||
return feedPostSliceItem
|
||||
}),
|
||||
items: item.items
|
||||
.slice(0, 6)
|
||||
.filter(subItem => {
|
||||
return !PINNED_POST_URIS[subItem.post.uri]
|
||||
})
|
||||
.map((subItem, i) => {
|
||||
const feedPostSliceItem: FeedPostSliceItem = {
|
||||
_reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
|
||||
uri: subItem.post.uri,
|
||||
post: subItem.post,
|
||||
record: subItem.record,
|
||||
moderation: moderations[i],
|
||||
parentAuthor: subItem.parentAuthor,
|
||||
isParentBlocked: subItem.isParentBlocked,
|
||||
isParentNotFound: subItem.isParentNotFound,
|
||||
}
|
||||
return feedPostSliceItem
|
||||
}),
|
||||
}
|
||||
if (slice.isIncompleteThread && slice.items.length >= 3) {
|
||||
const beforeLast = slice.items.length - 2
|
||||
@@ -202,6 +244,7 @@ export function useFeedPreviews(
|
||||
key: slice.items[0]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: 0,
|
||||
feed: page.feed,
|
||||
showReplyTo: false,
|
||||
hideTopBorder: rowIndex === 0,
|
||||
})
|
||||
@@ -215,6 +258,7 @@ export function useFeedPreviews(
|
||||
key: slice.items[beforeLast]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: beforeLast,
|
||||
feed: page.feed,
|
||||
showReplyTo:
|
||||
slice.items[beforeLast].parentAuthor?.did !==
|
||||
slice.items[beforeLast].post.author.did,
|
||||
@@ -225,6 +269,7 @@ export function useFeedPreviews(
|
||||
key: slice.items[last]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: last,
|
||||
feed: page.feed,
|
||||
showReplyTo: false,
|
||||
hideTopBorder: false,
|
||||
})
|
||||
@@ -235,6 +280,7 @@ export function useFeedPreviews(
|
||||
key: slice.items[i]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: i,
|
||||
feed: page.feed,
|
||||
showReplyTo: i === 0,
|
||||
hideTopBorder: i === 0 && rowIndex === 0,
|
||||
})
|
||||
|
||||
+11
-11
@@ -1,18 +1,18 @@
|
||||
import {useCallback, useEffect, useMemo, useRef} from 'react'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyUnspeccedGetPopularFeedGenerators,
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyGraphDefs,
|
||||
type AppBskyUnspeccedGetPopularFeedGenerators,
|
||||
AtUri,
|
||||
moderateFeedGenerator,
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
InfiniteData,
|
||||
type InfiniteData,
|
||||
keepPreviousData,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
type QueryClient,
|
||||
type QueryKey,
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
@@ -28,7 +28,7 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {router} from '#/routes'
|
||||
import {useModerationOpts} from '../preferences/moderation-opts'
|
||||
import {FeedDescriptor} from './post-feed'
|
||||
import {type FeedDescriptor} from './post-feed'
|
||||
import {precacheResolvedUri} from './resolve-uri'
|
||||
|
||||
export type FeedSourceFeedInfo = {
|
||||
@@ -203,12 +203,12 @@ export const KNOWN_AUTHED_ONLY_FEEDS = [
|
||||
'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why
|
||||
]
|
||||
|
||||
type GetPopularFeedsOptions = {limit?: number}
|
||||
type GetPopularFeedsOptions = {limit?: number; enabled?: boolean}
|
||||
|
||||
export function createGetPopularFeedsQueryKey(
|
||||
options?: GetPopularFeedsOptions,
|
||||
) {
|
||||
return ['getPopularFeeds', options]
|
||||
return ['getPopularFeeds', options?.limit]
|
||||
}
|
||||
|
||||
export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
|
||||
@@ -237,7 +237,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
|
||||
QueryKey,
|
||||
string | undefined
|
||||
>({
|
||||
enabled: Boolean(moderationOpts),
|
||||
enabled: Boolean(moderationOpts) && options?.enabled !== false,
|
||||
queryKey: createGetPopularFeedsQueryKey(options),
|
||||
queryFn: async ({pageParam}) => {
|
||||
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
|
||||
@@ -11,18 +11,17 @@ import {useAgent} from '#/state/session'
|
||||
|
||||
export const DEFAULT_LIMIT = 5
|
||||
|
||||
export const createGetTrendsQueryKey = () => ['suggested-feeds']
|
||||
export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds']
|
||||
|
||||
export function useGetSuggestedFeedsQuery() {
|
||||
export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
|
||||
const agent = useAgent()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const savedFeeds = preferences?.savedFeeds
|
||||
|
||||
return useQuery({
|
||||
enabled: !!preferences,
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: createGetTrendsQueryKey(),
|
||||
enabled: !!preferences && enabled !== false,
|
||||
staleTime: STALE.MINUTES.THREE,
|
||||
queryKey: createGetSuggestedFeedsQueryKey(),
|
||||
queryFn: async () => {
|
||||
const contentLangs = getContentLanguages().join(',')
|
||||
const {data} = await agent.app.bsky.unspecced.getSuggestedFeeds(
|
||||
|
||||
@@ -13,12 +13,17 @@ import {STALE} from '#/state/queries'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
export type QueryProps = {category?: string | null}
|
||||
export type QueryProps = {
|
||||
category?: string | null
|
||||
limit?: number
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users'
|
||||
export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [
|
||||
getSuggestedUsersQueryKeyRoot,
|
||||
...Object.values(props),
|
||||
props.category,
|
||||
props.limit,
|
||||
]
|
||||
|
||||
export function useGetSuggestedUsersQuery(props: QueryProps) {
|
||||
@@ -26,15 +31,15 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
|
||||
return useQuery({
|
||||
enabled: !!preferences,
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
enabled: !!preferences && props.enabled !== false,
|
||||
staleTime: STALE.MINUTES.THREE,
|
||||
queryKey: createGetSuggestedUsersQueryKey(props),
|
||||
queryFn: async () => {
|
||||
const contentLangs = getContentLanguages().join(',')
|
||||
const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
|
||||
{
|
||||
category: props.category ?? undefined,
|
||||
limit: props.limit || 10,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@@ -52,7 +57,7 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
|
||||
): Generator<AppBskyActorDefs.ProfileView, void> {
|
||||
const responses =
|
||||
queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsers.OutputSchema>({
|
||||
queryKey: [getSuggestedUsersQueryKeyRoot],
|
||||
|
||||
@@ -25,7 +25,6 @@ export function useGetTrendsQuery() {
|
||||
|
||||
return useQuery({
|
||||
enabled: !!preferences,
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: STALE.MINUTES.THREE,
|
||||
queryKey: createGetTrendsQueryKey(),
|
||||
queryFn: async () => {
|
||||
|
||||
@@ -13,15 +13,14 @@ export const createSuggestedStarterPacksQueryKey = () => [
|
||||
'suggested-starter-packs',
|
||||
]
|
||||
|
||||
export function useSuggestedStarterPacksQuery() {
|
||||
export function useSuggestedStarterPacksQuery({enabled}: {enabled?: boolean}) {
|
||||
const agent = useAgent()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const contentLangs = getContentLanguages().join(',')
|
||||
|
||||
return useQuery({
|
||||
enabled: !!preferences,
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
enabled: !!preferences && enabled !== false,
|
||||
staleTime: STALE.MINUTES.THREE,
|
||||
queryKey: createSuggestedStarterPacksQueryKey(),
|
||||
async queryFn() {
|
||||
const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks(
|
||||
|
||||
@@ -3,33 +3,88 @@ import {BskyAgent} from '@atproto/api'
|
||||
import {logger} from '#/logger'
|
||||
import {device} from '#/storage'
|
||||
|
||||
export const BR_LABELER = 'did:plc:ekitcvx7uwnauoqy5oest3hm'
|
||||
export const DE_LABELER = 'did:plc:r55ow3tocux5kafs5dq445fy'
|
||||
export const ADDITIONAL_LABELERS_MAP: {
|
||||
export const BR_LABELER = 'did:plc:ekitcvx7uwnauoqy5oest3hm' // Brazil
|
||||
export const DE_LABELER = 'did:plc:r55ow3tocux5kafs5dq445fy' // Germany
|
||||
export const RU_LABELER = 'did:plc:crm2agcxvvlj6hilnjdc4hox' // Russia
|
||||
export const UK_LABELER = 'did:plc:gvkp7euswjjrctjmqwhhfzif' // United Kingdom
|
||||
export const AU_LABELER = 'did:plc:dsynw7isrf2eqlhcjx3ffnmt' // Australia
|
||||
export const TR_LABELER = 'did:plc:cquoj7aozvmkud2gifeinkda' // Turkey
|
||||
export const JP_LABELER = 'did:plc:vhgppeyjwgrr37vm4v6ggd5a' // Japan
|
||||
export const ES_LABELER = 'did:plc:zlbbuj5nov4ixhvgl3bj47em' // Spain
|
||||
export const PK_LABELER = 'did:plc:zrp6a3tvprrsgawsbswbxu7m' // Pakistan
|
||||
export const IN_LABELER = 'did:plc:srr4rdvgzkbx6t7fxqtt6j5t' // India
|
||||
|
||||
/**
|
||||
* For all EU countries
|
||||
*/
|
||||
export const EU_LABELER = 'did:plc:z57lz5dhgz2dkjogoysm3vut'
|
||||
|
||||
const MODERATION_AUTHORITIES: {
|
||||
[countryCode: string]: string[]
|
||||
} = {
|
||||
BR: [BR_LABELER],
|
||||
DE: [DE_LABELER],
|
||||
BR: [BR_LABELER], // Brazil
|
||||
RU: [RU_LABELER], // Russia
|
||||
UK: [UK_LABELER], // United Kingdom
|
||||
AU: [AU_LABELER], // Australia
|
||||
TR: [TR_LABELER], // Turkey
|
||||
JP: [JP_LABELER], // Japan
|
||||
PK: [PK_LABELER], // Pakistan
|
||||
IN: [IN_LABELER], // India
|
||||
|
||||
// EU countries
|
||||
AT: [EU_LABELER], // Austria
|
||||
BE: [EU_LABELER], // Belgium
|
||||
BG: [EU_LABELER], // Bulgaria
|
||||
HR: [EU_LABELER], // Croatia
|
||||
CY: [EU_LABELER], // Cyprus
|
||||
CZ: [EU_LABELER], // Czech Republic
|
||||
DK: [EU_LABELER], // Denmark
|
||||
EE: [EU_LABELER], // Estonia
|
||||
FI: [EU_LABELER], // Finland
|
||||
FR: [EU_LABELER], // France
|
||||
DE: [EU_LABELER, DE_LABELER], // Germany
|
||||
GR: [EU_LABELER], // Greece
|
||||
HU: [EU_LABELER], // Hungary
|
||||
IE: [EU_LABELER], // Ireland
|
||||
IT: [EU_LABELER], // Italy
|
||||
LV: [EU_LABELER], // Latvia
|
||||
LT: [EU_LABELER], // Lithuania
|
||||
LU: [EU_LABELER], // Luxembourg
|
||||
MT: [EU_LABELER], // Malta
|
||||
NL: [EU_LABELER], // Netherlands
|
||||
PL: [EU_LABELER], // Poland
|
||||
PT: [EU_LABELER], // Portugal
|
||||
RO: [EU_LABELER], // Romania
|
||||
SK: [EU_LABELER], // Slovakia
|
||||
SI: [EU_LABELER], // Slovenia
|
||||
ES: [EU_LABELER, ES_LABELER], // Spain
|
||||
SE: [EU_LABELER], // Sweden
|
||||
}
|
||||
export const ALL_ADDITIONAL_LABELERS = Object.values(
|
||||
ADDITIONAL_LABELERS_MAP,
|
||||
).flat()
|
||||
export const NON_CONFIGURABLE_LABELERS = [BR_LABELER, DE_LABELER]
|
||||
|
||||
const MODERATION_AUTHORITIES_DIDS = Array.from(
|
||||
new Set(Object.values(MODERATION_AUTHORITIES).flat()),
|
||||
)
|
||||
|
||||
export function isNonConfigurableModerationAuthority(did: string) {
|
||||
return NON_CONFIGURABLE_LABELERS.includes(did)
|
||||
return MODERATION_AUTHORITIES_DIDS.includes(did)
|
||||
}
|
||||
|
||||
export function configureAdditionalModerationAuthorities() {
|
||||
const geolocation = device.get(['geolocation'])
|
||||
let additionalLabelers: string[] = ALL_ADDITIONAL_LABELERS
|
||||
// default to all
|
||||
let additionalLabelers: string[] = MODERATION_AUTHORITIES_DIDS
|
||||
|
||||
if (geolocation?.countryCode) {
|
||||
additionalLabelers = ADDITIONAL_LABELERS_MAP[geolocation.countryCode] ?? []
|
||||
// overwrite with only those necessary
|
||||
additionalLabelers = MODERATION_AUTHORITIES[geolocation.countryCode] ?? []
|
||||
} else {
|
||||
logger.info(`no geolocation, cannot apply mod authorities`)
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
additionalLabelers = []
|
||||
}
|
||||
|
||||
const appLabelers = Array.from(
|
||||
new Set([...BskyAgent.appLabelers, ...additionalLabelers]),
|
||||
)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, {memo, useMemo} from 'react'
|
||||
import {
|
||||
GestureResponderEvent,
|
||||
type GestureResponderEvent,
|
||||
StyleSheet,
|
||||
Text as RNText,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
type AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedThreadgate,
|
||||
type AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
ModerationDecision,
|
||||
type ModerationDecision,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
@@ -26,9 +26,14 @@ import {countLines} from '#/lib/strings/helpers'
|
||||
import {niceDate} from '#/lib/strings/time'
|
||||
import {s} from '#/lib/styles'
|
||||
import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers'
|
||||
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
||||
import {logger} from '#/logger'
|
||||
import {
|
||||
POST_TOMBSTONE,
|
||||
type Shadow,
|
||||
usePostShadow,
|
||||
} from '#/state/cache/post-shadow'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {ThreadPost} from '#/state/queries/post-thread'
|
||||
import {type ThreadPost} from '#/state/queries/post-thread'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
@@ -52,7 +57,7 @@ import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {PostHider} from '#/components/moderation/PostHider'
|
||||
import {AppModerationCause} from '#/components/Pills'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
||||
@@ -747,14 +752,29 @@ function ExpandedPostDetails({
|
||||
const {_, i18n} = useLingui()
|
||||
const openLink = useOpenLink()
|
||||
const isRootPost = !('reply' in post.record)
|
||||
const langPrefs = useLanguagePrefs()
|
||||
|
||||
const onTranslatePress = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
e.preventDefault()
|
||||
openLink(translatorUrl, true)
|
||||
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
) {
|
||||
logger.metric('translate', {
|
||||
sourceLanguages: post.record.langs ?? [],
|
||||
targetLanguage: langPrefs.primaryLanguage,
|
||||
textLength: post.record.text.length,
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
[openLink, translatorUrl],
|
||||
[openLink, translatorUrl, langPrefs, post],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
} from 'react-native'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
type AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedThreadgate,
|
||||
type AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
RichText as RichTextAPI,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -20,7 +20,10 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {getCurrentRoute} from '#/lib/routes/helpers'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
type NavigationProp,
|
||||
} from '#/lib/routes/types'
|
||||
import {shareText, shareUrl} from '#/lib/sharing'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
@@ -28,7 +31,7 @@ import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {Shadow} from '#/state/cache/post-shadow'
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
@@ -85,6 +88,7 @@ import {
|
||||
useReportDialogControl,
|
||||
} from '#/components/moderation/ReportDialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import * as Toast from '../Toast'
|
||||
|
||||
let PostDropdownMenuItems = ({
|
||||
@@ -248,7 +252,20 @@ let PostDropdownMenuItems = ({
|
||||
|
||||
const onPressTranslate = React.useCallback(async () => {
|
||||
await openLink(translatorUrl, true)
|
||||
}, [openLink, translatorUrl])
|
||||
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
) {
|
||||
logger.metric('translate', {
|
||||
sourceLanguages: post.record.langs ?? [],
|
||||
targetLanguage: langPrefs.primaryLanguage,
|
||||
textLength: post.record.text.length,
|
||||
})
|
||||
}
|
||||
}, [openLink, translatorUrl, langPrefs, post])
|
||||
|
||||
const onHidePost = React.useCallback(() => {
|
||||
hidePost({uri: postUri})
|
||||
|
||||
Reference in New Issue
Block a user