Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 213070d6c9 | |||
| d2c9f3ff92 | |||
| d8ea31c1d5 |
+2
-3
@@ -1,5 +1,3 @@
|
||||
const reactCompilerConfig = require('./react-compiler.config.js')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
@@ -81,7 +79,8 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
'react-compiler/react-compiler': ['error', reactCompilerConfig],
|
||||
// TODO: Reenable when we figure out why it gets stuck on CI.
|
||||
// 'react-compiler/react-compiler': 'error',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
|
||||
@@ -17,8 +17,8 @@ appId: xyz.blueskyweb.app
|
||||
- inputText: "Post with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn: "Content warnings"
|
||||
- tapOn: "Content warnings"
|
||||
- tapOn:
|
||||
id: "labelsBtn"
|
||||
- tapOn: "Porn"
|
||||
- tapOn:
|
||||
label: "Tap on confirm"
|
||||
|
||||
@@ -29,6 +29,7 @@ appId: xyz.blueskyweb.app
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn: "Not now"
|
||||
- tapOn:
|
||||
id: "handleInput"
|
||||
- inputText: "e2e-test"
|
||||
|
||||
@@ -23,7 +23,6 @@ appId: xyz.blueskyweb.app
|
||||
id: "editDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn: "About"
|
||||
@@ -46,7 +45,6 @@ appId: xyz.blueskyweb.app
|
||||
- eraseText
|
||||
- inputText: "They bad"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: Bad Ppl
|
||||
@@ -62,7 +60,6 @@ appId: xyz.blueskyweb.app
|
||||
id: "editDescriptionInput"
|
||||
- eraseText
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible:
|
||||
@@ -90,7 +87,6 @@ appId: xyz.blueskyweb.app
|
||||
id: "editDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn: "About"
|
||||
|
||||
@@ -23,4 +23,5 @@ appId: xyz.blueskyweb.app
|
||||
id: "loginPasswordInput"
|
||||
- inputText: "hunter2"
|
||||
- pressKey: Enter
|
||||
- tapOn: "Not now"
|
||||
- assertVisible: "Following"
|
||||
|
||||
@@ -22,7 +22,6 @@ appId: xyz.blueskyweb.app
|
||||
id: "editDescriptionInput"
|
||||
- inputText: "Shhh"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
|
||||
# view modlist
|
||||
- assertVisible: "Muted Users"
|
||||
|
||||
@@ -13,7 +13,7 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn: "Select an avatar"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn:
|
||||
point: "50%,22%"
|
||||
point: "16%,22%"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Choose"
|
||||
- waitForAnimationToEnd
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M15.439 3.148a1 1 0 0 1 .41.645l.568 3.22a7 7 0 1 1-6.174 10.97L4.32 19.027a1 1 0 0 1-1.159-.811L1.078 6.398a1 1 0 0 1 .81-1.158l12.803-2.258a1 1 0 0 1 .748.166ZM9.325 16.114A7 7 0 0 1 9 14c0-1.56.51-3 1.372-4.164l-6.456 1.139 1.041 5.909 4.368-.77ZM3.568 9.005l10.833-1.91-.347-1.97L3.22 7.036l.347 1.97ZM16 9a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm0 2a1 1 0 0 1 1 1v1.586l1.374 1.374a1 1 0 0 1-1.414 1.414l-1.667-1.667A1 1 0 0 1 15 14v-2a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 589 B |
+6
-3
@@ -1,5 +1,3 @@
|
||||
const reactCompilerConfig = require('./react-compiler.config.js')
|
||||
|
||||
module.exports = function (api) {
|
||||
api.cache(true)
|
||||
const isTestEnv = process.env.NODE_ENV === 'test'
|
||||
@@ -19,7 +17,12 @@ module.exports = function (api) {
|
||||
],
|
||||
plugins: [
|
||||
'macros',
|
||||
['babel-plugin-react-compiler', reactCompilerConfig],
|
||||
[
|
||||
'babel-plugin-react-compiler',
|
||||
{
|
||||
runtimeModule: 'react-compiler-runtime',
|
||||
},
|
||||
],
|
||||
[
|
||||
'module:react-native-dotenv',
|
||||
{
|
||||
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
const React = require('react')
|
||||
const $empty = Symbol.for('react.memo_cache_sentinel')
|
||||
/**
|
||||
* DANGER: this hook is NEVER meant to be called directly!
|
||||
*
|
||||
* Note that this is a temporary userspace implementation of this function
|
||||
* from React 19. It is not as efficient and may invalidate more frequently
|
||||
* than the official API. Please upgrade to React 19 as soon as you can.
|
||||
**/
|
||||
export function c(size) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return React.useState(() => {
|
||||
const $ = new Array(size)
|
||||
for (let ii = 0; ii < size; ii++) {
|
||||
$[ii] = $empty
|
||||
}
|
||||
// @ts-ignore
|
||||
$[$empty] = true
|
||||
return $
|
||||
})[0]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "react-compiler-runtime",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -38,10 +38,9 @@ cfg.transformer.getTransformOptions = async () => ({
|
||||
inlineRequires: true,
|
||||
nonInlinedRequires: [
|
||||
// We can remove this option and rely on the default after
|
||||
// https://github.com/facebook/metro/pull/1390 is released.
|
||||
// https://github.com/facebook/metro/pull/1126 is released.
|
||||
'React',
|
||||
'react',
|
||||
'react-compiler-runtime',
|
||||
'react/jsx-dev-runtime',
|
||||
'react/jsx-runtime',
|
||||
'react-native',
|
||||
|
||||
+5
-5
@@ -166,14 +166,14 @@
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"psl": "^1.9.0",
|
||||
"react": "18.2.0",
|
||||
"react-compiler-runtime": "^19.0.0-beta-a7bf2bd-20241110",
|
||||
"react-compiler-runtime": "file:./lib/react-compiler-runtime",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-keyed-flatten-children": "^3.0.0",
|
||||
"react-native": "0.74.1",
|
||||
"react-native-compressor": "^1.8.24",
|
||||
"react-native-date-picker": "^4.4.2",
|
||||
"react-native-drawer-layout": "^4.0.1",
|
||||
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
||||
"react-native-gesture-handler": "2.20.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-image-crop-picker": "0.41.2",
|
||||
@@ -243,7 +243,7 @@
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"babel-plugin-react-compiler": "^19.0.0-beta-a7bf2bd-20241110",
|
||||
"babel-plugin-react-compiler": "^0.0.0-experimental-592953e-20240517",
|
||||
"babel-plugin-react-native-web": "^0.18.12",
|
||||
"babel-preset-expo": "^10.0.0",
|
||||
"eslint": "^8.19.0",
|
||||
@@ -251,10 +251,9 @@
|
||||
"eslint-plugin-ft-flow": "^2.0.3",
|
||||
"eslint-plugin-lingui": "^0.2.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-a7bf2bd-20241110",
|
||||
"eslint-plugin-react-compiler": "^0.0.0-experimental-c8b3f72-20240517",
|
||||
"eslint-plugin-react-native-a11y": "^3.3.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"file-loader": "6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"husky": "^8.0.3",
|
||||
"is-ci": "^3.0.1",
|
||||
@@ -266,6 +265,7 @@
|
||||
"prettier": "^2.8.3",
|
||||
"react-native-dotenv": "^3.3.1",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"svgo": "^3.3.2",
|
||||
"ts-node": "^10.9.1",
|
||||
|
||||
@@ -9,7 +9,7 @@ index caa5540..c5d4e67 100644
|
||||
- type != nil && [type length] > 0 ? type : @"application/octet-stream",
|
||||
+ ![type isEqual:[NSNull null]] && [type length] > 0 ? type : @"application/octet-stream",
|
||||
[data base64EncodedStringWithOptions:0]];
|
||||
|
||||
|
||||
resolve(text);
|
||||
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
|
||||
index b0d71dc..41b9a0e 100644
|
||||
@@ -18,7 +18,7 @@ index b0d71dc..41b9a0e 100644
|
||||
@@ -377,10 +377,6 @@ - (void)textInputDidBeginEditing
|
||||
self.backedTextInputView.attributedText = [NSAttributedString new];
|
||||
}
|
||||
|
||||
|
||||
- if (_selectTextOnFocus) {
|
||||
- [self.backedTextInputView selectAll:nil];
|
||||
- }
|
||||
@@ -35,7 +35,7 @@ index b0d71dc..41b9a0e 100644
|
||||
+ [self.backedTextInputView selectAll:nil];
|
||||
+ }
|
||||
}
|
||||
|
||||
|
||||
- (void)reactBlur
|
||||
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||
index e9b330f..ec5f58c 100644
|
||||
@@ -48,10 +48,10 @@ index e9b330f..ec5f58c 100644
|
||||
+@property (nonatomic, copy) UIColor *customTintColor;
|
||||
+
|
||||
+- (void)forwarderBeginRefreshing;
|
||||
|
||||
|
||||
@end
|
||||
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
index b09e653..d2b4e05 100644
|
||||
index b09e653..288e60c 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
@@ -22,6 +22,7 @@ @implementation RCTRefreshControl {
|
||||
@@ -60,12 +60,12 @@ index b09e653..d2b4e05 100644
|
||||
CGFloat _progressViewOffset;
|
||||
+ UIColor *_customTintColor;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)init
|
||||
@@ -56,6 +57,12 @@ - (void)layoutSubviews
|
||||
_isInitialRender = false;
|
||||
}
|
||||
|
||||
|
||||
+- (void)didMoveToSuperview
|
||||
+{
|
||||
+ [super didMoveToSuperview];
|
||||
@@ -78,7 +78,7 @@ index b09e653..d2b4e05 100644
|
||||
@@ -203,4 +210,58 @@ - (void)refreshControlValueChanged
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+- (void)setCustomTintColor:(UIColor *)customTintColor
|
||||
+{
|
||||
+ _customTintColor = customTintColor;
|
||||
@@ -139,139 +139,26 @@ 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
|
||||
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
|
||||
-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
|
||||
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor)
|
||||
RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat)
|
||||
|
||||
|
||||
+RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor)
|
||||
+
|
||||
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
|
||||
{
|
||||
[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 1aead51..39e6244 100644
|
||||
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
|
||||
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
|
||||
@@ -159,31 +159,6 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
|
||||
return !shouldDisableScrollInteraction;
|
||||
}
|
||||
|
||||
-/*
|
||||
- * Automatically centers the content such that if the content is smaller than the
|
||||
- * 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.
|
||||
- */
|
||||
-- (void)setContentOffset:(CGPoint)contentOffset
|
||||
-{
|
||||
- UIView *contentView = [self contentView];
|
||||
- if (contentView && _centerContent && !CGSizeEqualToSize(contentView.frame.size, CGSizeZero)) {
|
||||
- CGSize subviewSize = contentView.frame.size;
|
||||
- CGSize scrollViewSize = self.bounds.size;
|
||||
- if (subviewSize.width <= scrollViewSize.width) {
|
||||
- contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0;
|
||||
- }
|
||||
- if (subviewSize.height <= scrollViewSize.height) {
|
||||
- contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0;
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- super.contentOffset = CGPointMake(
|
||||
- RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
|
||||
- RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
|
||||
-}
|
||||
-
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
// Preserving and revalidating `contentOffset`.
|
||||
@@ -427,6 +402,11 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
|
||||
// Does nothing
|
||||
}
|
||||
|
||||
+- (void)setFrame:(CGRect)frame {
|
||||
+ [super setFrame:frame];
|
||||
+ [self centerContentIfNeeded];
|
||||
+}
|
||||
+
|
||||
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
|
||||
{
|
||||
[super insertReactSubview:view atIndex:atIndex];
|
||||
@@ -444,6 +424,8 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
|
||||
RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
|
||||
[_scrollView addSubview:view];
|
||||
}
|
||||
+
|
||||
+ [self centerContentIfNeeded];
|
||||
}
|
||||
|
||||
- (void)removeReactSubview:(UIView *)subview
|
||||
@@ -652,9 +634,46 @@ -(void)delegateMethod : (UIScrollView *)scrollView \
|
||||
}
|
||||
|
||||
RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin)
|
||||
-RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
|
||||
RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
|
||||
|
||||
+-(void)scrollViewDidZoom : (UIScrollView *)scrollView
|
||||
+{
|
||||
+ [self centerContentIfNeeded];
|
||||
+
|
||||
+ RCT_SEND_SCROLL_EVENT(onScroll, nil);
|
||||
+ RCT_FORWARD_SCROLL_EVENT(scrollViewDidZoom : scrollView);
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * Automatically centers the content such that if the content is smaller than the
|
||||
+ * 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.
|
||||
+ *
|
||||
+ * PATCHED: This deviates from the original React Native implementation to fix two issues:
|
||||
+ *
|
||||
+ * - The scroll view was swallowing any taps immediately after pinching
|
||||
+ * - The scroll view was jittering when crossing the full screen threshold while pinching
|
||||
+ *
|
||||
+ * This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/.
|
||||
+ */
|
||||
+-(void)centerContentIfNeeded
|
||||
+{
|
||||
+ if (_scrollView.centerContent &&
|
||||
+ !CGSizeEqualToSize(self.contentSize, CGSizeZero) &&
|
||||
+ !CGSizeEqualToSize(self.bounds.size, CGSizeZero)
|
||||
+ ) {
|
||||
+ CGFloat top = 0, left = 0;
|
||||
+ if (self.contentSize.width < self.bounds.size.width) {
|
||||
+ left = (self.bounds.size.width - self.contentSize.width) * 0.5f;
|
||||
+ }
|
||||
+ if (self.contentSize.height < self.bounds.size.height) {
|
||||
+ top = (self.bounds.size.height - self.contentSize.height) * 0.5f;
|
||||
+ }
|
||||
+ _scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
- (void)addScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
|
||||
{
|
||||
[_scrollListeners addObject:scrollListener];
|
||||
@@ -913,6 +932,7 @@ - (void)updateContentSizeIfNeeded
|
||||
CGSize contentSize = self.contentSize;
|
||||
if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
|
||||
_scrollView.contentSize = contentSize;
|
||||
+ [self centerContentIfNeeded];
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
|
||||
index 5f5e1ab..aac00b6 100644
|
||||
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
|
||||
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
|
||||
@@ -99,8 +99,9 @@ public class JavaTimerManager {
|
||||
}
|
||||
|
||||
|
||||
// If the JS thread is busy for multiple frames we cancel any other pending runnable.
|
||||
- if (mCurrentIdleCallbackRunnable != null) {
|
||||
- mCurrentIdleCallbackRunnable.cancel();
|
||||
@@ -279,5 +166,5 @@ index 5f5e1ab..aac00b6 100644
|
||||
+ if (currentRunnable != null) {
|
||||
+ currentRunnable.cancel();
|
||||
}
|
||||
|
||||
|
||||
mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
target: '18',
|
||||
environment: {
|
||||
enableTreatRefLikeIdentifiersAsRefs: true,
|
||||
validateRefAccessDuringRender: false, // TODO: Make it `true`.
|
||||
},
|
||||
}
|
||||
+33
-40
@@ -56,6 +56,7 @@ type Props = {
|
||||
const AnimatedLogo = Animated.createAnimatedComponent(Logo)
|
||||
|
||||
export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
'use no memo'
|
||||
const insets = useSafeAreaInsets()
|
||||
const intro = useSharedValue(0)
|
||||
const outroLogo = useSharedValue(0)
|
||||
@@ -80,40 +81,40 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
|
||||
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
|
||||
},
|
||||
{
|
||||
scale: interpolate(
|
||||
outroLogo.get(),
|
||||
outroLogo.value,
|
||||
[0, 0.08, 1],
|
||||
[1, 0.8, 500],
|
||||
'clamp',
|
||||
),
|
||||
},
|
||||
],
|
||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
|
||||
}
|
||||
})
|
||||
const bottomLogoAnimation = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
|
||||
}
|
||||
})
|
||||
const reducedLogoAnimation = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
|
||||
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
|
||||
},
|
||||
],
|
||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
|
||||
}
|
||||
})
|
||||
|
||||
const logoWrapperAnimation = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: interpolate(
|
||||
outroAppOpacity.get(),
|
||||
outroAppOpacity.value,
|
||||
[0, 0.1, 0.2, 1],
|
||||
[1, 1, 0, 0],
|
||||
'clamp',
|
||||
@@ -125,11 +126,11 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'),
|
||||
scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'),
|
||||
},
|
||||
],
|
||||
opacity: interpolate(
|
||||
outroAppOpacity.get(),
|
||||
outroAppOpacity.value,
|
||||
[0, 0.1, 0.2, 1],
|
||||
[0, 0, 1, 1],
|
||||
'clamp',
|
||||
@@ -145,37 +146,29 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
if (isReady) {
|
||||
SplashScreen.hideAsync()
|
||||
.then(() => {
|
||||
intro.set(() =>
|
||||
withTiming(
|
||||
1,
|
||||
{duration: 400, easing: Easing.out(Easing.cubic)},
|
||||
async () => {
|
||||
// set these values to check animation at specific point
|
||||
// outroLogo.set(0.1)
|
||||
// outroApp.set(0.1)
|
||||
outroLogo.set(() =>
|
||||
withTiming(
|
||||
1,
|
||||
{duration: 1200, easing: Easing.in(Easing.cubic)},
|
||||
() => {
|
||||
runOnJS(onFinish)()
|
||||
},
|
||||
),
|
||||
)
|
||||
outroApp.set(() =>
|
||||
withTiming(1, {
|
||||
duration: 1200,
|
||||
easing: Easing.inOut(Easing.cubic),
|
||||
}),
|
||||
)
|
||||
outroAppOpacity.set(() =>
|
||||
withTiming(1, {
|
||||
duration: 1200,
|
||||
easing: Easing.in(Easing.cubic),
|
||||
}),
|
||||
)
|
||||
},
|
||||
),
|
||||
intro.value = withTiming(
|
||||
1,
|
||||
{duration: 400, easing: Easing.out(Easing.cubic)},
|
||||
async () => {
|
||||
// set these values to check animation at specific point
|
||||
// outroLogo.value = 0.1
|
||||
// outroApp.value = 0.1
|
||||
outroLogo.value = withTiming(
|
||||
1,
|
||||
{duration: 1200, easing: Easing.in(Easing.cubic)},
|
||||
() => {
|
||||
runOnJS(onFinish)()
|
||||
},
|
||||
)
|
||||
outroApp.value = withTiming(1, {
|
||||
duration: 1200,
|
||||
easing: Easing.inOut(Easing.cubic),
|
||||
})
|
||||
outroAppOpacity.value = withTiming(1, {
|
||||
duration: 1200,
|
||||
easing: Easing.in(Easing.cubic),
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
@@ -29,10 +29,10 @@ export function useDialogControl(): DialogOuterProps['control'] {
|
||||
const {activeDialogs} = useDialogStateContext()
|
||||
|
||||
React.useEffect(() => {
|
||||
const map = activeDialogs.current
|
||||
map.set(id, control)
|
||||
activeDialogs.current.set(id, control)
|
||||
return () => {
|
||||
map.delete(id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
activeDialogs.current.delete(id)
|
||||
}
|
||||
}, [id, activeDialogs])
|
||||
|
||||
|
||||
@@ -17,12 +17,13 @@ export function Loader(props: Props) {
|
||||
const rotation = useSharedValue(0)
|
||||
|
||||
const animatedStyles = useAnimatedStyle(() => ({
|
||||
transform: [{rotate: rotation.get() + 'deg'}],
|
||||
transform: [{rotate: rotation.value + 'deg'}],
|
||||
}))
|
||||
|
||||
React.useEffect(() => {
|
||||
rotation.set(() =>
|
||||
withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1),
|
||||
rotation.value = withRepeat(
|
||||
withTiming(360, {duration: 500, easing: Easing.linear}),
|
||||
-1,
|
||||
)
|
||||
}, [rotation])
|
||||
|
||||
|
||||
@@ -55,15 +55,13 @@ export const ProgressGuideToast = React.forwardRef<
|
||||
|
||||
// animate the opacity then set isOpen to false when done
|
||||
const setIsntOpen = () => setIsOpen(false)
|
||||
opacity.set(() =>
|
||||
withTiming(
|
||||
0,
|
||||
{
|
||||
duration: 400,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
},
|
||||
() => runOnJS(setIsntOpen)(),
|
||||
),
|
||||
opacity.value = withTiming(
|
||||
0,
|
||||
{
|
||||
duration: 400,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
},
|
||||
() => runOnJS(setIsntOpen)(),
|
||||
)
|
||||
}, [setIsOpen, opacity])
|
||||
|
||||
@@ -73,24 +71,20 @@ export const ProgressGuideToast = React.forwardRef<
|
||||
|
||||
// animate the vertical translation, the opacity, and the checkmark
|
||||
const playCheckmark = () => animatedCheckRef.current?.play()
|
||||
opacity.set(0)
|
||||
opacity.set(() =>
|
||||
withTiming(
|
||||
1,
|
||||
{
|
||||
duration: 100,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
},
|
||||
() => runOnJS(playCheckmark)(),
|
||||
),
|
||||
)
|
||||
translateY.set(0)
|
||||
translateY.set(() =>
|
||||
withTiming(insets.top + 10, {
|
||||
duration: 500,
|
||||
opacity.value = 0
|
||||
opacity.value = withTiming(
|
||||
1,
|
||||
{
|
||||
duration: 100,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
}),
|
||||
},
|
||||
() => runOnJS(playCheckmark)(),
|
||||
)
|
||||
translateY.value = 0
|
||||
translateY.value = withTiming(insets.top + 10, {
|
||||
duration: 500,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
})
|
||||
|
||||
// start the countdown timer to autoclose
|
||||
timeoutRef.current = setTimeout(close, visibleDuration || 5e3)
|
||||
@@ -120,8 +114,8 @@ export const ProgressGuideToast = React.forwardRef<
|
||||
}, [winDim.width])
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{translateY: translateY.get()}],
|
||||
opacity: opacity.get(),
|
||||
transform: [{translateY: translateY.value}],
|
||||
opacity: opacity.value,
|
||||
}))
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,25 +32,21 @@ export const AnimatedCheck = React.forwardRef<
|
||||
const checkAnim = useSharedValue(0)
|
||||
|
||||
const circleAnimatedProps = useAnimatedProps(() => ({
|
||||
strokeDashoffset: 166 - circleAnim.get() * 166,
|
||||
strokeDashoffset: 166 - circleAnim.value * 166,
|
||||
}))
|
||||
const checkAnimatedProps = useAnimatedProps(() => ({
|
||||
strokeDashoffset: 48 - 48 * checkAnim.get(),
|
||||
strokeDashoffset: 48 - 48 * checkAnim.value,
|
||||
}))
|
||||
|
||||
const play = React.useCallback(
|
||||
(cb?: () => void) => {
|
||||
circleAnim.set(0)
|
||||
checkAnim.set(0)
|
||||
circleAnim.value = 0
|
||||
checkAnim.value = 0
|
||||
|
||||
circleAnim.set(() =>
|
||||
withTiming(1, {duration: 500, easing: Easing.linear}),
|
||||
)
|
||||
checkAnim.set(() =>
|
||||
withDelay(
|
||||
500,
|
||||
withTiming(1, {duration: 300, easing: Easing.linear}, cb),
|
||||
),
|
||||
circleAnim.value = withTiming(1, {duration: 500, easing: Easing.linear})
|
||||
checkAnim.value = withDelay(
|
||||
500,
|
||||
withTiming(1, {duration: 300, easing: Easing.linear}, cb),
|
||||
)
|
||||
},
|
||||
[circleAnim, checkAnim],
|
||||
|
||||
@@ -146,17 +146,18 @@ export function Inner({
|
||||
<ErrorMessage message={error} />
|
||||
</View>
|
||||
) : null}
|
||||
{currentStep === 'StepOne' ? (
|
||||
<View>
|
||||
{reasonText ? (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_md, a.leading_snug]}>{reasonText}</Text>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
Don't have access to{' '}
|
||||
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
|
||||
{currentAccount?.email}
|
||||
</Text>
|
||||
?{' '}
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
{currentStep === 'StepOne' ? (
|
||||
<>
|
||||
{!reasonText ? (
|
||||
<>
|
||||
<Trans>
|
||||
You'll receive an email at{' '}
|
||||
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
|
||||
{currentAccount?.email}
|
||||
</Text>{' '}
|
||||
to verify it's you.
|
||||
</Trans>{' '}
|
||||
<InlineLinkText
|
||||
to="#"
|
||||
label={_(msg`Change email address`)}
|
||||
@@ -168,41 +169,17 @@ export function Inner({
|
||||
})
|
||||
return false
|
||||
}}>
|
||||
<Trans>Change your email address</Trans>
|
||||
<Trans>Need to change it?</Trans>
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
You'll receive an email at{' '}
|
||||
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
|
||||
{currentAccount?.email}
|
||||
</Text>{' '}
|
||||
to verify it's you.
|
||||
</Trans>{' '}
|
||||
<InlineLinkText
|
||||
to="#"
|
||||
label={_(msg`Change email address`)}
|
||||
style={[a.text_md, a.leading_snug]}
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
control.close(() => {
|
||||
openModal({name: 'change-email'})
|
||||
})
|
||||
return false
|
||||
}}>
|
||||
<Trans>Need to change it?</Trans>
|
||||
</InlineLinkText>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
{uiStrings[currentStep].message}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
reasonText
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
uiStrings[currentStep].message
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
{currentStep === 'StepTwo' ? (
|
||||
<View>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {AppearanceToggleButtonGroup} from '#/screens/Settings/AppearanceSettings'
|
||||
import {atoms as a, useAlf, useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
|
||||
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function NeueTypography() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const nuxDialogs = useNuxDialogContext()
|
||||
const control = Dialog.useDialogControl()
|
||||
const {fonts} = useAlf()
|
||||
|
||||
Dialog.useAutoOpen(control, 3e3)
|
||||
|
||||
const onClose = React.useCallback(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
}, [nuxDialogs])
|
||||
|
||||
const onChangeFontFamily = React.useCallback(
|
||||
(values: string[]) => {
|
||||
const next = values[0] === 'system' ? 'system' : 'theme'
|
||||
fonts.setFontFamily(next)
|
||||
},
|
||||
[fonts],
|
||||
)
|
||||
|
||||
const onChangeFontScale = React.useCallback(
|
||||
(values: string[]) => {
|
||||
const next = values[0] || ('0' as any)
|
||||
fonts.setFontScale(next)
|
||||
},
|
||||
[fonts],
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control} onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
|
||||
<View style={[a.gap_xl]}>
|
||||
<View style={[a.gap_md]}>
|
||||
<Text style={[a.text_3xl, a.font_heavy]}>
|
||||
<Trans>New font settings ✨</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_lg, a.leading_snug, {maxWidth: 400}]}>
|
||||
<Trans>
|
||||
We're introducing a new theme font, along with adjustable font
|
||||
sizing.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
You can adjust these in your Appearance Settings later.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View style={[a.gap_lg]}>
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Font`)}
|
||||
description={_(
|
||||
msg`For the best experience, we recommend using the theme font.`,
|
||||
)}
|
||||
icon={Aa}
|
||||
items={[
|
||||
{
|
||||
label: _(msg`System`),
|
||||
name: 'system',
|
||||
},
|
||||
{
|
||||
label: _(msg`Theme`),
|
||||
name: 'theme',
|
||||
},
|
||||
]}
|
||||
values={[fonts.family]}
|
||||
onChange={onChangeFontFamily}
|
||||
/>
|
||||
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Font size`)}
|
||||
icon={TextSize}
|
||||
items={[
|
||||
{
|
||||
label: _(msg`Smaller`),
|
||||
name: '-1',
|
||||
},
|
||||
{
|
||||
label: _(msg`Default`),
|
||||
name: '0',
|
||||
},
|
||||
{
|
||||
label: _(msg`Larger`),
|
||||
name: '1',
|
||||
},
|
||||
]}
|
||||
values={[fonts.scale]}
|
||||
onChange={onChangeFontScale}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {useOnboardingState} from '#/state/shell'
|
||||
/*
|
||||
* NUXs
|
||||
*/
|
||||
import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
|
||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
@@ -35,7 +36,19 @@ const queuedNuxs: {
|
||||
currentProfile: AppBskyActorDefs.ProfileViewDetailed
|
||||
preferences: UsePreferencesQueryResponse
|
||||
}) => boolean
|
||||
}[] = []
|
||||
}[] = [
|
||||
{
|
||||
id: Nux.NeueTypography,
|
||||
enabled(props) {
|
||||
if (props.currentProfile.createdAt) {
|
||||
if (new Date(props.currentProfile.createdAt) < new Date('2024-10-09')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const Context = React.createContext<Context>({
|
||||
activeNux: undefined,
|
||||
@@ -53,14 +66,7 @@ export function NuxDialogs() {
|
||||
const onboardingActive = useOnboardingState().isActive
|
||||
|
||||
const isLoading =
|
||||
onboardingActive ||
|
||||
!currentAccount ||
|
||||
!preferences ||
|
||||
!profile ||
|
||||
// Profile isn't legit ready until createdAt is a real date.
|
||||
!profile.createdAt ||
|
||||
profile.createdAt === '0001-01-01T00:00:00.000Z' // TODO: Fix this in AppView.
|
||||
|
||||
!currentAccount || !preferences || !profile || onboardingActive
|
||||
return !isLoading ? (
|
||||
<Inner
|
||||
currentAccount={currentAccount}
|
||||
@@ -100,7 +106,6 @@ function Inner({
|
||||
|
||||
if (IS_DEV && typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
window.clearNuxDialog = (id: Nux) => {
|
||||
if (!IS_DEV || !id) return
|
||||
removeNuxs([id])
|
||||
@@ -169,7 +174,7 @@ function Inner({
|
||||
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
|
||||
{activeNux === Nux.NeueTypography && <NeueTypography />}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function ActionsWrapper({
|
||||
const scale = useSharedValue(1)
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{scale: scale.get()}],
|
||||
transform: [{scale: scale.value}],
|
||||
}))
|
||||
|
||||
const open = React.useCallback(() => {
|
||||
@@ -46,7 +46,7 @@ export function ActionsWrapper({
|
||||
const shrink = React.useCallback(() => {
|
||||
'worklet'
|
||||
cancelAnimation(scale)
|
||||
scale.set(() => withTiming(1, {duration: 200}))
|
||||
scale.value = withTiming(1, {duration: 200})
|
||||
}, [scale])
|
||||
|
||||
const doubleTapGesture = Gesture.Tap()
|
||||
@@ -58,13 +58,11 @@ export function ActionsWrapper({
|
||||
const pressAndHoldGesture = Gesture.LongPress()
|
||||
.onStart(() => {
|
||||
'worklet'
|
||||
scale.set(() =>
|
||||
withTiming(1.05, {duration: 200}, finished => {
|
||||
if (!finished) return
|
||||
runOnJS(open)()
|
||||
shrink()
|
||||
}),
|
||||
)
|
||||
scale.value = withTiming(1.05, {duration: 200}, finished => {
|
||||
if (!finished) return
|
||||
runOnJS(open)()
|
||||
shrink()
|
||||
})
|
||||
})
|
||||
.onTouchesUp(shrink)
|
||||
.onTouchesMove(shrink)
|
||||
|
||||
@@ -42,12 +42,12 @@ export function ChatEmptyPill() {
|
||||
|
||||
const onPressIn = React.useCallback(() => {
|
||||
if (isWeb) return
|
||||
scale.set(() => withTiming(1.075, {duration: 100}))
|
||||
scale.value = withTiming(1.075, {duration: 100})
|
||||
}, [scale])
|
||||
|
||||
const onPressOut = React.useCallback(() => {
|
||||
if (isWeb) return
|
||||
scale.set(() => withTiming(1, {duration: 100}))
|
||||
scale.value = withTiming(1, {duration: 100})
|
||||
}, [scale])
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
@@ -61,7 +61,7 @@ export function ChatEmptyPill() {
|
||||
}, [playHaptic, prompts.length])
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{scale: scale.get()}],
|
||||
transform: [{scale: scale.value}],
|
||||
}))
|
||||
|
||||
return (
|
||||
|
||||
@@ -62,7 +62,7 @@ export let MessageMenu = ({
|
||||
message.text,
|
||||
langPrefs.primaryLanguage,
|
||||
)
|
||||
openLink(translatorUrl, true)
|
||||
openLink(translatorUrl)
|
||||
}, [langPrefs.primaryLanguage, message.text, openLink])
|
||||
|
||||
const onDelete = React.useCallback(() => {
|
||||
|
||||
@@ -35,12 +35,12 @@ export function NewMessagesPill({
|
||||
|
||||
const onPressIn = React.useCallback(() => {
|
||||
if (isWeb) return
|
||||
scale.set(() => withTiming(1.075, {duration: 100}))
|
||||
scale.value = withTiming(1.075, {duration: 100})
|
||||
}, [scale])
|
||||
|
||||
const onPressOut = React.useCallback(() => {
|
||||
if (isWeb) return
|
||||
scale.set(() => withTiming(1, {duration: 100}))
|
||||
scale.value = withTiming(1, {duration: 100})
|
||||
}, [scale])
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
@@ -49,7 +49,7 @@ export function NewMessagesPill({
|
||||
}, [onPressInner, playHaptic])
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{scale: scale.get()}],
|
||||
transform: [{scale: scale.value}],
|
||||
}))
|
||||
|
||||
return (
|
||||
|
||||
@@ -278,7 +278,7 @@ export function SearchablePeopleList({
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View style={web([a.pt_xs])}>
|
||||
<View style={[, web([a.pt_xs])]}>
|
||||
<SearchInput
|
||||
inputRef={inputRef}
|
||||
value={searchText}
|
||||
@@ -313,7 +313,6 @@ export function SearchablePeopleList({
|
||||
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
|
||||
native({height: '100%'}),
|
||||
]}
|
||||
webInnerContentContainerStyle={a.py_0}
|
||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||
keyboardDismissMode="on-drag"
|
||||
/>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const CalendarClock_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M15.439 3.148a1 1 0 0 1 .41.645l.568 3.22a7 7 0 1 1-6.174 10.97L4.32 19.027a1 1 0 0 1-1.159-.811L1.078 6.398a1 1 0 0 1 .81-1.158l12.803-2.258a1 1 0 0 1 .748.166ZM9.325 16.114A7 7 0 0 1 9 14c0-1.56.51-3 1.372-4.164l-6.456 1.139 1.041 5.909 4.368-.77ZM3.568 9.005l10.833-1.91-.347-1.97L3.22 7.036l.347 1.97ZM16 9a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm0 2a1 1 0 0 1 1 1v1.586l1.374 1.374a1 1 0 0 1-1.414 1.414l-1.667-1.667A1 1 0 0 1 15 14v-2a1 1 0 0 1 1-1Z',
|
||||
})
|
||||
@@ -68,8 +68,6 @@ export function ContentHider({
|
||||
if (hasAdultContentLabel) {
|
||||
return false
|
||||
}
|
||||
// https://github.com/facebook/react/issues/31569
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
hasAdultContentLabel = true
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -22,7 +22,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_sm,
|
||||
a.gap_md,
|
||||
a.px_lg,
|
||||
a.py_lg,
|
||||
a.justify_between,
|
||||
@@ -74,9 +74,10 @@ export function Buttons({
|
||||
hideLabel?: string
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
|
||||
return (
|
||||
<View style={[{minHeight: 35}, a.w_full]}>
|
||||
<View style={[{minHeight: 35}, gtPhone ? undefined : a.w_full]}>
|
||||
<ToggleButton.Group
|
||||
label={_(
|
||||
msg`Configure content filtering setting for category: ${name}`,
|
||||
@@ -258,7 +259,7 @@ export function LabelerLabelPreference({
|
||||
</Content>
|
||||
|
||||
{showConfig && (
|
||||
<>
|
||||
<View style={[gtPhone ? undefined : a.w_full]}>
|
||||
{cantConfigure ? (
|
||||
<View
|
||||
style={[
|
||||
@@ -289,7 +290,7 @@ export function LabelerLabelPreference({
|
||||
hideLabel={hideLabel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</View>
|
||||
)}
|
||||
</Outer>
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ export const STARTER_PACK_MAX_SIZE = 150
|
||||
// code and update this number with each release until we can get the
|
||||
// server route done.
|
||||
// -prf
|
||||
export const JOINED_THIS_WEEK = 3500000 // estimate as of 11/16/24
|
||||
export const JOINED_THIS_WEEK = 650000 // estimate as of 10/28/24
|
||||
|
||||
export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
|
||||
'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at
|
||||
|
||||
@@ -61,7 +61,7 @@ export function GestureActionView({
|
||||
const clampedTransX = useDerivedValue(() => {
|
||||
const min = actions.leftFirst ? -MAX_WIDTH : 0
|
||||
const max = actions.rightFirst ? MAX_WIDTH : 0
|
||||
return clamp(transX.get(), min, max)
|
||||
return clamp(transX.value, min, max)
|
||||
})
|
||||
|
||||
const iconScale = useSharedValue(1)
|
||||
@@ -75,23 +75,21 @@ export function GestureActionView({
|
||||
return
|
||||
}
|
||||
|
||||
iconScale.set(() =>
|
||||
withSequence(
|
||||
withTiming(1.2, {duration: 175}),
|
||||
withTiming(1, {duration: 100}),
|
||||
),
|
||||
iconScale.value = withSequence(
|
||||
withTiming(1.2, {duration: 175}),
|
||||
withTiming(1, {duration: 100}),
|
||||
)
|
||||
}
|
||||
|
||||
useAnimatedReaction(
|
||||
() => transX,
|
||||
() => {
|
||||
if (transX.get() === 0) {
|
||||
if (transX.value === 0) {
|
||||
runOnJS(setActiveAction)(null)
|
||||
} else if (transX.get() < 0) {
|
||||
} else if (transX.value < 0) {
|
||||
if (
|
||||
actions.leftSecond &&
|
||||
transX.get() <= -actions.leftSecond.threshold
|
||||
transX.value <= -actions.leftSecond.threshold
|
||||
) {
|
||||
if (activeAction !== 'leftSecond') {
|
||||
runOnJS(setActiveAction)('leftSecond')
|
||||
@@ -99,10 +97,10 @@ export function GestureActionView({
|
||||
} else if (activeAction !== 'leftFirst') {
|
||||
runOnJS(setActiveAction)('leftFirst')
|
||||
}
|
||||
} else if (transX.get() > 0) {
|
||||
} else if (transX.value > 0) {
|
||||
if (
|
||||
actions.rightSecond &&
|
||||
transX.get() > actions.rightSecond.threshold
|
||||
transX.value > actions.rightSecond.threshold
|
||||
) {
|
||||
if (activeAction !== 'rightSecond') {
|
||||
runOnJS(setActiveAction)('rightSecond')
|
||||
@@ -121,44 +119,44 @@ export function GestureActionView({
|
||||
.activeOffsetY([-200, 200])
|
||||
.onStart(() => {
|
||||
'worklet'
|
||||
isActive.set(true)
|
||||
isActive.value = true
|
||||
})
|
||||
.onChange(e => {
|
||||
'worklet'
|
||||
transX.set(e.translationX)
|
||||
transX.value = e.translationX
|
||||
|
||||
if (e.translationX < 0) {
|
||||
// Left side
|
||||
if (actions.leftSecond) {
|
||||
if (
|
||||
e.translationX <= -actions.leftSecond.threshold &&
|
||||
!hitSecond.get()
|
||||
!hitSecond.value
|
||||
) {
|
||||
runPopAnimation()
|
||||
runOnJS(haptic)()
|
||||
hitSecond.set(true)
|
||||
hitSecond.value = true
|
||||
} else if (
|
||||
hitSecond.get() &&
|
||||
hitSecond.value &&
|
||||
e.translationX > -actions.leftSecond.threshold
|
||||
) {
|
||||
runPopAnimation()
|
||||
hitSecond.set(false)
|
||||
hitSecond.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (!hitSecond.get() && actions.leftFirst) {
|
||||
if (!hitSecond.value && actions.leftFirst) {
|
||||
if (
|
||||
e.translationX <= -actions.leftFirst.threshold &&
|
||||
!hitFirst.get()
|
||||
!hitFirst.value
|
||||
) {
|
||||
runPopAnimation()
|
||||
runOnJS(haptic)()
|
||||
hitFirst.set(true)
|
||||
hitFirst.value = true
|
||||
} else if (
|
||||
hitFirst.get() &&
|
||||
hitFirst.value &&
|
||||
e.translationX > -actions.leftFirst.threshold
|
||||
) {
|
||||
hitFirst.set(false)
|
||||
hitFirst.value = false
|
||||
}
|
||||
}
|
||||
} else if (e.translationX > 0) {
|
||||
@@ -166,33 +164,33 @@ export function GestureActionView({
|
||||
if (actions.rightSecond) {
|
||||
if (
|
||||
e.translationX >= actions.rightSecond.threshold &&
|
||||
!hitSecond.get()
|
||||
!hitSecond.value
|
||||
) {
|
||||
runPopAnimation()
|
||||
runOnJS(haptic)()
|
||||
hitSecond.set(true)
|
||||
hitSecond.value = true
|
||||
} else if (
|
||||
hitSecond.get() &&
|
||||
hitSecond.value &&
|
||||
e.translationX < actions.rightSecond.threshold
|
||||
) {
|
||||
runPopAnimation()
|
||||
hitSecond.set(false)
|
||||
hitSecond.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (!hitSecond.get() && actions.rightFirst) {
|
||||
if (!hitSecond.value && actions.rightFirst) {
|
||||
if (
|
||||
e.translationX >= actions.rightFirst.threshold &&
|
||||
!hitFirst.get()
|
||||
!hitFirst.value
|
||||
) {
|
||||
runPopAnimation()
|
||||
runOnJS(haptic)()
|
||||
hitFirst.set(true)
|
||||
hitFirst.value = true
|
||||
} else if (
|
||||
hitFirst.get() &&
|
||||
hitFirst.value &&
|
||||
e.translationX < actions.rightFirst.threshold
|
||||
) {
|
||||
hitFirst.set(false)
|
||||
hitFirst.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,29 +198,29 @@ export function GestureActionView({
|
||||
.onEnd(e => {
|
||||
'worklet'
|
||||
if (e.translationX < 0) {
|
||||
if (hitSecond.get() && actions.leftSecond) {
|
||||
if (hitSecond.value && actions.leftSecond) {
|
||||
runOnJS(actions.leftSecond.action)()
|
||||
} else if (hitFirst.get() && actions.leftFirst) {
|
||||
} else if (hitFirst.value && actions.leftFirst) {
|
||||
runOnJS(actions.leftFirst.action)()
|
||||
}
|
||||
} else if (e.translationX > 0) {
|
||||
if (hitSecond.get() && actions.rightSecond) {
|
||||
if (hitSecond.value && actions.rightSecond) {
|
||||
runOnJS(actions.rightSecond.action)()
|
||||
} else if (hitSecond.get() && actions.rightFirst) {
|
||||
} else if (hitSecond.value && actions.rightFirst) {
|
||||
runOnJS(actions.rightFirst.action)()
|
||||
}
|
||||
}
|
||||
transX.set(() => withTiming(0, {duration: 200}))
|
||||
hitFirst.set(false)
|
||||
hitSecond.set(false)
|
||||
isActive.set(false)
|
||||
transX.value = withTiming(0, {duration: 200})
|
||||
hitFirst.value = false
|
||||
hitSecond.value = false
|
||||
isActive.value = false
|
||||
})
|
||||
|
||||
const composedGesture = Gesture.Simultaneous(panGesture)
|
||||
|
||||
const animatedSliderStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [{translateX: clampedTransX.get()}],
|
||||
transform: [{translateX: clampedTransX.value}],
|
||||
}
|
||||
})
|
||||
|
||||
@@ -276,7 +274,7 @@ export function GestureActionView({
|
||||
const animatedBackgroundStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
backgroundColor: interpolateColor(
|
||||
clampedTransX.get(),
|
||||
clampedTransX.value,
|
||||
interpolation.inputRange,
|
||||
// @ts-expect-error - Weird type expected by reanimated, but this is okay
|
||||
interpolation.outputRange,
|
||||
@@ -285,10 +283,10 @@ export function GestureActionView({
|
||||
})
|
||||
|
||||
const animatedIconStyle = useAnimatedStyle(() => {
|
||||
const absTransX = Math.abs(clampedTransX.get())
|
||||
const absTransX = Math.abs(clampedTransX.value)
|
||||
return {
|
||||
opacity: interpolate(absTransX, [0, 75], [0.15, 1]),
|
||||
transform: [{scale: iconScale.get()}],
|
||||
transform: [{scale: iconScale.value}],
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
|
||||
import Animated, {
|
||||
cancelAnimation,
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useReducedMotion,
|
||||
useSharedValue,
|
||||
@@ -31,25 +32,27 @@ export function PressableScale({
|
||||
const scale = useSharedValue(1)
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{scale: scale.get()}],
|
||||
transform: [{scale: scale.value}],
|
||||
}))
|
||||
|
||||
return (
|
||||
<AnimatedPressable
|
||||
accessibilityRole="button"
|
||||
onPressIn={e => {
|
||||
'worklet'
|
||||
if (onPressIn) {
|
||||
onPressIn(e)
|
||||
runOnJS(onPressIn)(e)
|
||||
}
|
||||
cancelAnimation(scale)
|
||||
scale.set(() => withTiming(targetScale, {duration: 100}))
|
||||
scale.value = withTiming(targetScale, {duration: 100})
|
||||
}}
|
||||
onPressOut={e => {
|
||||
'worklet'
|
||||
if (onPressOut) {
|
||||
onPressOut(e)
|
||||
runOnJS(onPressOut)(e)
|
||||
}
|
||||
cancelAnimation(scale)
|
||||
scale.set(() => withTiming(1, {duration: 100}))
|
||||
scale.value = withTiming(1, {duration: 100})
|
||||
}}
|
||||
style={[!reducedMotion && animatedStyle, style]}
|
||||
{...rest}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {useServiceConfigQuery} from '#/state/queries/email-verification-required'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {BSKY_SERVICE} from '../constants'
|
||||
import {getHostnameFromUrl} from '../strings/url-helpers'
|
||||
@@ -8,25 +7,13 @@ export function useEmail() {
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const {data: serviceConfig} = useServiceConfigQuery()
|
||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||
|
||||
const checkEmailConfirmed = !!serviceConfig?.checkEmailConfirmed
|
||||
|
||||
// Date set for 11 AM PST on the 18th of November
|
||||
const isNewEnough =
|
||||
!!profile?.createdAt &&
|
||||
Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z')
|
||||
|
||||
const isSelfHost =
|
||||
serviceConfig?.checkEmailConfirmed &&
|
||||
currentAccount &&
|
||||
getHostnameFromUrl(currentAccount.service) !==
|
||||
getHostnameFromUrl(BSKY_SERVICE)
|
||||
|
||||
const needsEmailVerification =
|
||||
!isSelfHost &&
|
||||
checkEmailConfirmed &&
|
||||
!currentAccount?.emailConfirmed &&
|
||||
isNewEnough
|
||||
const needsEmailVerification = !isSelfHost && !currentAccount?.emailConfirmed
|
||||
|
||||
return {needsEmailVerification}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,15 @@ export function useMinimalShellHeaderTransform() {
|
||||
const {headerHeight} = useShellLayout()
|
||||
|
||||
const headerTransform = useAnimatedStyle(() => {
|
||||
const headerModeValue = headerMode.get()
|
||||
return {
|
||||
pointerEvents: headerModeValue === 0 ? 'auto' : 'none',
|
||||
opacity: Math.pow(1 - headerModeValue, 2),
|
||||
pointerEvents: headerMode.value === 0 ? 'auto' : 'none',
|
||||
opacity: Math.pow(1 - headerMode.value, 2),
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(
|
||||
headerModeValue,
|
||||
headerMode.value,
|
||||
[0, 1],
|
||||
[0, -headerHeight.get()],
|
||||
[0, -headerHeight.value],
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -34,16 +33,15 @@ export function useMinimalShellFooterTransform() {
|
||||
const {footerHeight} = useShellLayout()
|
||||
|
||||
const footerTransform = useAnimatedStyle(() => {
|
||||
const footerModeValue = footerMode.get()
|
||||
return {
|
||||
pointerEvents: footerModeValue === 0 ? 'auto' : 'none',
|
||||
opacity: Math.pow(1 - footerModeValue, 2),
|
||||
pointerEvents: footerMode.value === 0 ? 'auto' : 'none',
|
||||
opacity: Math.pow(1 - footerMode.value, 2),
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(
|
||||
footerModeValue,
|
||||
footerMode.value,
|
||||
[0, 1],
|
||||
[0, footerHeight.get()],
|
||||
[0, footerHeight.value],
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -60,7 +58,7 @@ export function useMinimalShellFabTransform() {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(footerMode.get(), [0, 1], [-44, 0]),
|
||||
translateY: interpolate(footerMode.value, [0, 1], [-44, 0]),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react'
|
||||
|
||||
/**
|
||||
* Helper hook to run persistent timers on views
|
||||
*/
|
||||
export function useTimer(time: number, handler: () => void) {
|
||||
const timer = React.useRef<undefined | NodeJS.Timeout>(undefined)
|
||||
|
||||
// function to restart the timer
|
||||
const reset = React.useCallback(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
}
|
||||
timer.current = setTimeout(handler, time)
|
||||
}, [time, timer, handler])
|
||||
|
||||
// function to cancel the timer
|
||||
const cancel = React.useCallback(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
timer.current = undefined
|
||||
}
|
||||
}, [timer])
|
||||
|
||||
// start the timer immediately
|
||||
React.useEffect(() => {
|
||||
reset()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return [reset, cancel]
|
||||
}
|
||||
@@ -9,24 +9,20 @@ if ('scrollRestoration' in history) {
|
||||
|
||||
function createInitialScrollState() {
|
||||
return {
|
||||
// Not used for rendering.
|
||||
// Treat it as a ref so that we can mutate it without upsetting the compiler.
|
||||
current: {
|
||||
scrollYs: new Map(),
|
||||
focusedKey: null as string | null,
|
||||
},
|
||||
scrollYs: new Map(),
|
||||
focusedKey: null as string | null,
|
||||
}
|
||||
}
|
||||
|
||||
export function useWebScrollRestoration() {
|
||||
const [ref] = useState(createInitialScrollState)
|
||||
const [state] = useState(createInitialScrollState)
|
||||
const navigation = useNavigation()
|
||||
|
||||
useEffect(() => {
|
||||
function onDispatch() {
|
||||
if (ref.current.focusedKey) {
|
||||
if (state.focusedKey) {
|
||||
// Remember where we were for later.
|
||||
ref.current.scrollYs.set(ref.current.focusedKey, window.scrollY)
|
||||
state.scrollYs.set(state.focusedKey, window.scrollY)
|
||||
// TODO: Strictly speaking, this is a leak. We never clean up.
|
||||
// This is because I'm not sure when it's appropriate to clean it up.
|
||||
// It doesn't seem like popstate is enough because it can still Forward-Back again.
|
||||
@@ -40,17 +36,17 @@ export function useWebScrollRestoration() {
|
||||
return () => {
|
||||
navigation.removeListener('__unsafe_action__' as any, onDispatch)
|
||||
}
|
||||
}, [ref, navigation])
|
||||
}, [state, navigation])
|
||||
|
||||
const screenListeners = useMemo(
|
||||
() => ({
|
||||
focus(e: EventArg<'focus', boolean | undefined, unknown>) {
|
||||
const scrollY = ref.current.scrollYs.get(e.target) ?? 0
|
||||
const scrollY = state.scrollYs.get(e.target) ?? 0
|
||||
window.scrollTo(0, scrollY)
|
||||
ref.current.focusedKey = e.target ?? null
|
||||
state.focusedKey = e.target ?? null
|
||||
},
|
||||
}),
|
||||
[ref],
|
||||
[state],
|
||||
)
|
||||
return screenListeners
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import {t} from '@lingui/macro'
|
||||
|
||||
export function cleanError(str: any): string {
|
||||
if (!str) {
|
||||
return ''
|
||||
@@ -8,17 +6,13 @@ export function cleanError(str: any): string {
|
||||
str = str.toString()
|
||||
}
|
||||
if (isNetworkError(str)) {
|
||||
return t`Unable to connect. Please check your internet connection and try again.`
|
||||
return 'Unable to connect. Please check your internet connection and try again.'
|
||||
}
|
||||
if (
|
||||
str.includes('Upstream Failure') ||
|
||||
str.includes('NotEnoughResources') ||
|
||||
str.includes('pipethrough network error')
|
||||
) {
|
||||
return t`The server appears to be experiencing issues. Please try again in a few moments.`
|
||||
if (str.includes('Upstream Failure')) {
|
||||
return 'The server appears to be experiencing issues. Please try again in a few moments.'
|
||||
}
|
||||
if (str.includes('Bad token scope')) {
|
||||
return t`This feature is not available while using an App Password. Please sign in with your main password.`
|
||||
return 'This feature is not available while using an App Password. Please sign in with your main password.'
|
||||
}
|
||||
if (str.startsWith('Error: ')) {
|
||||
return str.slice('Error: '.length)
|
||||
|
||||
+983
-1546
File diff suppressed because it is too large
Load Diff
+926
-1497
File diff suppressed because it is too large
Load Diff
+891
-1498
File diff suppressed because it is too large
Load Diff
+891
-1498
File diff suppressed because it is too large
Load Diff
+996
-1562
File diff suppressed because it is too large
Load Diff
+925
-1496
File diff suppressed because it is too large
Load Diff
+1268
-1602
File diff suppressed because it is too large
Load Diff
+2159
-1590
File diff suppressed because it is too large
Load Diff
+2462
-3029
File diff suppressed because it is too large
Load Diff
+927
-1507
File diff suppressed because it is too large
Load Diff
+927
-1498
File diff suppressed because it is too large
Load Diff
+1290
-1627
File diff suppressed because it is too large
Load Diff
+1145
-1252
File diff suppressed because it is too large
Load Diff
+1152
-1268
File diff suppressed because it is too large
Load Diff
+987
-1558
File diff suppressed because it is too large
Load Diff
+926
-1497
File diff suppressed because it is too large
Load Diff
+1303
-2005
File diff suppressed because it is too large
Load Diff
+925
-1496
File diff suppressed because it is too large
Load Diff
+924
-1495
File diff suppressed because it is too large
Load Diff
+1721
-1829
File diff suppressed because it is too large
Load Diff
+1797
-1905
File diff suppressed because it is too large
Load Diff
+1612
-1720
File diff suppressed because it is too large
Load Diff
@@ -108,22 +108,22 @@ export function MessageInput({
|
||||
const measurement = measure(inputRef)
|
||||
if (!measurement) return
|
||||
|
||||
const max = windowHeight - -keyboardHeight.get() - topInset - 150
|
||||
const max = windowHeight - -keyboardHeight.value - topInset - 150
|
||||
const availableSpace = max - measurement.height
|
||||
|
||||
maxHeight.set(max)
|
||||
isInputScrollable.set(availableSpace < 30)
|
||||
maxHeight.value = max
|
||||
isInputScrollable.value = availableSpace < 30
|
||||
},
|
||||
},
|
||||
[windowHeight, topInset],
|
||||
)
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
maxHeight: maxHeight.get(),
|
||||
maxHeight: maxHeight.value,
|
||||
}))
|
||||
|
||||
const animatedProps = useAnimatedProps(() => ({
|
||||
scrollEnabled: isInputScrollable.get(),
|
||||
scrollEnabled: isInputScrollable.value,
|
||||
}))
|
||||
|
||||
return (
|
||||
|
||||
@@ -145,7 +145,7 @@ export function MessagesList({
|
||||
(_: number, height: number) => {
|
||||
// Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
|
||||
// previous off whenever we add new content to the previous offset whenever we add new content to the list.
|
||||
if (isWeb && isAtTop.get() && hasScrolled) {
|
||||
if (isWeb && isAtTop.value && hasScrolled) {
|
||||
flatListRef.current?.scrollToOffset({
|
||||
offset: height - prevContentHeight.current,
|
||||
animated: false,
|
||||
@@ -153,7 +153,7 @@ export function MessagesList({
|
||||
}
|
||||
|
||||
// This number _must_ be the height of the MaybeLoader component
|
||||
if (height > 50 && isAtBottom.get()) {
|
||||
if (height > 50 && isAtBottom.value) {
|
||||
// If the size of the content is changing by more than the height of the screen, then we don't
|
||||
// want to scroll further than the start of all the new content. Since we are storing the previous offset,
|
||||
// we can just scroll the user to that offset and add a little bit of padding. We'll also show the pill
|
||||
@@ -161,7 +161,7 @@ export function MessagesList({
|
||||
if (
|
||||
didBackground.current &&
|
||||
hasScrolled &&
|
||||
height - prevContentHeight.current > layoutHeight.get() - 50 &&
|
||||
height - prevContentHeight.current > layoutHeight.value - 50 &&
|
||||
convoState.items.length - prevItemCount.current > 1
|
||||
) {
|
||||
flatListRef.current?.scrollToOffset({
|
||||
@@ -209,7 +209,7 @@ export function MessagesList({
|
||||
)
|
||||
|
||||
const onStartReached = useCallback(() => {
|
||||
if (hasScrolled && prevContentHeight.current > layoutHeight.get()) {
|
||||
if (hasScrolled && prevContentHeight.current > layoutHeight.value) {
|
||||
convoState.fetchMessageHistory()
|
||||
}
|
||||
}, [convoState, hasScrolled, layoutHeight])
|
||||
@@ -217,18 +217,18 @@ export function MessagesList({
|
||||
const onScroll = React.useCallback(
|
||||
(e: ReanimatedScrollEvent) => {
|
||||
'worklet'
|
||||
layoutHeight.set(e.layoutMeasurement.height)
|
||||
layoutHeight.value = e.layoutMeasurement.height
|
||||
const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height
|
||||
|
||||
// Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom
|
||||
// when a new message is added, hence the 100 pixel offset
|
||||
isAtBottom.set(e.contentSize.height - 100 < bottomOffset)
|
||||
isAtTop.set(e.contentOffset.y <= 1)
|
||||
isAtBottom.value = e.contentSize.height - 100 < bottomOffset
|
||||
isAtTop.value = e.contentOffset.y <= 1
|
||||
|
||||
if (
|
||||
newMessagesPill.show &&
|
||||
(e.contentOffset.y > newMessagesPill.startContentOffset + 200 ||
|
||||
isAtBottom.get())
|
||||
isAtBottom.value)
|
||||
) {
|
||||
runOnJS(setNewMessagesPill)({
|
||||
show: false,
|
||||
@@ -256,28 +256,28 @@ export function MessagesList({
|
||||
// Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should
|
||||
// just update the height here instead of having the `onMove` event do it (that event will not fire!)
|
||||
if (e.duration === 0) {
|
||||
layoutScrollWithoutAnimation.set(true)
|
||||
keyboardHeight.set(e.height)
|
||||
layoutScrollWithoutAnimation.value = true
|
||||
keyboardHeight.value = e.height
|
||||
} else {
|
||||
keyboardIsOpening.set(true)
|
||||
keyboardIsOpening.value = true
|
||||
}
|
||||
},
|
||||
onMove: e => {
|
||||
'worklet'
|
||||
keyboardHeight.set(e.height)
|
||||
keyboardHeight.value = e.height
|
||||
if (e.height > bottomOffset) {
|
||||
scrollTo(flatListRef, 0, 1e7, false)
|
||||
}
|
||||
},
|
||||
onEnd: () => {
|
||||
'worklet'
|
||||
keyboardIsOpening.set(false)
|
||||
keyboardIsOpening.value = false
|
||||
},
|
||||
})
|
||||
|
||||
const animatedListStyle = useAnimatedStyle(() => ({
|
||||
marginBottom:
|
||||
keyboardHeight.get() > bottomOffset ? keyboardHeight.get() : bottomOffset,
|
||||
keyboardHeight.value > bottomOffset ? keyboardHeight.value : bottomOffset,
|
||||
}))
|
||||
|
||||
// -- Message sending
|
||||
@@ -363,13 +363,13 @@ export function MessagesList({
|
||||
// -- List layout changes (opening emoji keyboard, etc.)
|
||||
const onListLayout = React.useCallback(
|
||||
(e: LayoutChangeEvent) => {
|
||||
layoutHeight.set(e.nativeEvent.layout.height)
|
||||
layoutHeight.value = e.nativeEvent.layout.height
|
||||
|
||||
if (isWeb || !keyboardIsOpening.get()) {
|
||||
if (isWeb || !keyboardIsOpening.value) {
|
||||
flatListRef.current?.scrollToEnd({
|
||||
animated: !layoutScrollWithoutAnimation.get(),
|
||||
animated: !layoutScrollWithoutAnimation.value,
|
||||
})
|
||||
layoutScrollWithoutAnimation.set(false)
|
||||
layoutScrollWithoutAnimation.value = false
|
||||
}
|
||||
},
|
||||
[
|
||||
|
||||
@@ -127,36 +127,31 @@ export function StepFinished() {
|
||||
})(),
|
||||
(async () => {
|
||||
const {imageUri, imageMime} = profileStepResults
|
||||
const blobPromise =
|
||||
imageUri && imageMime
|
||||
? uploadBlob(agent, imageUri, imageMime)
|
||||
: undefined
|
||||
|
||||
await agent.upsertProfile(async existing => {
|
||||
existing = existing ?? {}
|
||||
|
||||
if (blobPromise) {
|
||||
if (imageUri && imageMime) {
|
||||
const blobPromise = uploadBlob(agent, imageUri, imageMime)
|
||||
await agent.upsertProfile(async existing => {
|
||||
existing = existing ?? {}
|
||||
const res = await blobPromise
|
||||
if (res.data.blob) {
|
||||
existing.avatar = res.data.blob
|
||||
}
|
||||
}
|
||||
|
||||
if (starterPack) {
|
||||
existing.joinedViaStarterPack = {
|
||||
uri: starterPack.uri,
|
||||
cid: starterPack.cid,
|
||||
if (starterPack) {
|
||||
existing.joinedViaStarterPack = {
|
||||
uri: starterPack.uri,
|
||||
cid: starterPack.cid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
existing.displayName = ''
|
||||
// HACKFIX
|
||||
// creating a bunch of identical profile objects is breaking the relay
|
||||
// tossing this unspecced field onto it to reduce the size of the problem
|
||||
// -prf
|
||||
existing.createdAt = new Date().toISOString()
|
||||
return existing
|
||||
})
|
||||
existing.displayName = ''
|
||||
// HACKFIX
|
||||
// creating a bunch of identical profile objects is breaking the relay
|
||||
// tossing this unspecced field onto it to reduce the size of the problem
|
||||
// -prf
|
||||
existing.createdAt = new Date().toISOString()
|
||||
return existing
|
||||
})
|
||||
}
|
||||
|
||||
logEvent('onboarding:finished:avatarResult', {
|
||||
avatarResult: profileStepResults.isCreatedAvatar
|
||||
|
||||
@@ -45,7 +45,7 @@ function GrowableAvatarInner({
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
scale: interpolate(scrollY.get(), [-150, 0], [1.2, 1], {
|
||||
scale: interpolate(scrollY.value, [-150, 0], [1.2, 1], {
|
||||
extrapolateRight: Extrapolation.CLAMP,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ function GrowableBannerInner({
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
scale: interpolate(scrollY.get(), [-150, 0], [2, 1], {
|
||||
scale: interpolate(scrollY.value, [-150, 0], [2, 1], {
|
||||
extrapolateRight: Extrapolation.CLAMP,
|
||||
}),
|
||||
},
|
||||
@@ -76,7 +76,7 @@ function GrowableBannerInner({
|
||||
const animatedBlurViewProps = useAnimatedProps(() => {
|
||||
return {
|
||||
intensity: interpolate(
|
||||
scrollY.get(),
|
||||
scrollY.value,
|
||||
[-300, -65, -15],
|
||||
[50, 40, 0],
|
||||
Extrapolation.CLAMP,
|
||||
@@ -85,17 +85,16 @@ function GrowableBannerInner({
|
||||
})
|
||||
|
||||
const animatedSpinnerStyle = useAnimatedStyle(() => {
|
||||
const scrollYValue = scrollY.get()
|
||||
return {
|
||||
display: scrollYValue < 0 ? 'flex' : 'none',
|
||||
display: scrollY.value < 0 ? 'flex' : 'none',
|
||||
opacity: interpolate(
|
||||
scrollYValue,
|
||||
scrollY.value,
|
||||
[-60, -15],
|
||||
[1, 0],
|
||||
Extrapolation.CLAMP,
|
||||
),
|
||||
transform: [
|
||||
{translateY: interpolate(scrollYValue, [-150, 0], [-75, 0])},
|
||||
{translateY: interpolate(scrollY.value, [-150, 0], [-75, 0])},
|
||||
{rotate: '90deg'},
|
||||
],
|
||||
}
|
||||
@@ -104,7 +103,7 @@ function GrowableBannerInner({
|
||||
const animatedBackButtonStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], {
|
||||
translateY: interpolate(scrollY.value, [-150, 60], [-150, 60], {
|
||||
extrapolateRight: Extrapolation.CLAMP,
|
||||
}),
|
||||
},
|
||||
@@ -169,7 +168,7 @@ function useShouldAnimateSpinner({
|
||||
const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10)
|
||||
|
||||
useAnimatedReaction(
|
||||
() => scrollY.get() < -5,
|
||||
() => scrollY.value < -5,
|
||||
(value, prevValue) => {
|
||||
if (value !== prevValue) {
|
||||
runOnJS(setIsOverscrolled)(value)
|
||||
|
||||
@@ -80,8 +80,6 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||
React.useEffect(() => {
|
||||
if (state.pendingSubmit) {
|
||||
if (!state.pendingSubmit.mutableProcessed) {
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
state.pendingSubmit.mutableProcessed = true
|
||||
submit(state, dispatch)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const ACTIVE_POLL_INTERVAL = 4e3
|
||||
export const MESSAGE_SCREEN_POLL_INTERVAL = 30e3
|
||||
export const ACTIVE_POLL_INTERVAL = 3e3
|
||||
export const MESSAGE_SCREEN_POLL_INTERVAL = 10e3
|
||||
export const BACKGROUND_POLL_INTERVAL = 60e3
|
||||
export const INACTIVE_TIMEOUT = 60e3 * 5
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const DEFAULT_POLL_INTERVAL = 60e3
|
||||
export const DEFAULT_POLL_INTERVAL = 60e3 * 5
|
||||
export const BACKGROUND_POLL_INTERVAL = 60e3 * 5
|
||||
|
||||
@@ -173,8 +173,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
return wasActive
|
||||
})
|
||||
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
unstable__openModal = openModal
|
||||
unstable__closeModal = closeModal
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ const RQKEY_ROOT = 'actor-autocomplete'
|
||||
export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
|
||||
|
||||
export function useActorAutocompleteQuery(
|
||||
rawPrefix: string,
|
||||
prefix: string,
|
||||
maintainData?: boolean,
|
||||
limit?: number,
|
||||
) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const agent = useAgent()
|
||||
|
||||
let prefix = rawPrefix.toLowerCase().trim()
|
||||
prefix = prefix.toLowerCase().trim()
|
||||
if (prefix.endsWith('.')) {
|
||||
// Going from "foo" to "foo." should not clear matches.
|
||||
prefix = prefix.slice(0, -1)
|
||||
|
||||
@@ -5,11 +5,7 @@ import {STALE} from '#/state/queries'
|
||||
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
||||
import {useOnMarkAsRead} from '#/state/queries/messages/list-converations'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
ConvoListQueryData,
|
||||
getConvoFromQueryData,
|
||||
RQKEY as LIST_CONVOS_KEY,
|
||||
} from './list-converations'
|
||||
import {RQKEY as LIST_CONVOS_KEY} from './list-converations'
|
||||
|
||||
const RQKEY_ROOT = 'convo'
|
||||
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
|
||||
@@ -61,37 +57,8 @@ export function useMarkAsReadMutation() {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
optimisticUpdate(convoId)
|
||||
},
|
||||
onSuccess(_, {convoId}) {
|
||||
if (!convoId) return
|
||||
|
||||
queryClient.setQueryData(LIST_CONVOS_KEY, (old: ConvoListQueryData) => {
|
||||
if (!old) return old
|
||||
|
||||
const existingConvo = getConvoFromQueryData(convoId, old)
|
||||
|
||||
if (existingConvo) {
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map(page => {
|
||||
return {
|
||||
...page,
|
||||
convos: page.convos.map(convo => {
|
||||
if (convo.id === convoId) {
|
||||
return {
|
||||
...convo,
|
||||
unreadCount: 0,
|
||||
}
|
||||
}
|
||||
return convo
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
// If we somehow marked a convo as read that doesn't exist in the
|
||||
// list, then we don't need to do anything.
|
||||
}
|
||||
})
|
||||
onSettled() {
|
||||
queryClient.invalidateQueries({queryKey: LIST_CONVOS_KEY})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function useListConvosQuery({
|
||||
queryKey: RQKEY,
|
||||
queryFn: async ({pageParam}) => {
|
||||
const {data} = await agent.api.chat.bsky.convo.listConvos(
|
||||
{cursor: pageParam, limit: 20},
|
||||
{cursor: pageParam},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
|
||||
@@ -47,6 +47,9 @@ export function useListConvosQuery({
|
||||
},
|
||||
initialPageParam: undefined as RQPageParam,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
// refetch every 60 seconds since we can't get *all* info from the logs
|
||||
// i.e. reading chats on another device won't update the unread count
|
||||
refetchInterval: 60_000,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -177,11 +180,6 @@ export function ListConvosProviderInner({
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* We received a message from an conversation old enough that
|
||||
* it doesn't exist in the query cache, meaning we need to
|
||||
* refetch and bump the old convo to the top.
|
||||
*/
|
||||
debouncedRefetch()
|
||||
}
|
||||
})
|
||||
@@ -247,12 +245,12 @@ export function useUnreadMessageCount() {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
count,
|
||||
numUnread: count > 0 ? (count > 10 ? '10+' : String(count)) : undefined,
|
||||
numUnread: count > 0 ? (count > 30 ? '30+' : String(count)) : undefined,
|
||||
}
|
||||
}, [count])
|
||||
}
|
||||
|
||||
export type ConvoListQueryData = {
|
||||
type ConvoListQueryData = {
|
||||
pageParams: Array<string | undefined>
|
||||
pages: Array<ChatBskyConvoListConvos.OutputSchema>
|
||||
}
|
||||
@@ -303,7 +301,7 @@ function optimisticDelete(chatId: string, old: ConvoListQueryData) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getConvoFromQueryData(chatId: string, old: ConvoListQueryData) {
|
||||
function getConvoFromQueryData(chatId: string, old: ConvoListQueryData) {
|
||||
for (const page of old.pages) {
|
||||
for (const convo of page.convos) {
|
||||
if (convo.id === chatId) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
|
||||
import BroadcastChannel from '#/lib/broadcast'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {resetBadgeCount} from '#/lib/notifications/notifications'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
@@ -51,6 +50,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
|
||||
const [numUnread, setNumUnread] = React.useState('')
|
||||
|
||||
const checkUnreadRef = React.useRef<ApiContext['checkUnread'] | null>(null)
|
||||
const cacheRef = React.useRef<CachedFeedPage>({
|
||||
usableInFeed: false,
|
||||
syncedAt: new Date(),
|
||||
@@ -70,6 +70,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// periodic sync
|
||||
React.useEffect(() => {
|
||||
if (!hasSession || !checkUnreadRef.current) {
|
||||
return
|
||||
}
|
||||
checkUnreadRef.current() // fire on init
|
||||
const interval = setInterval(
|
||||
() => checkUnreadRef.current?.({isPoll: true}),
|
||||
UPDATE_INTERVAL,
|
||||
)
|
||||
return () => clearInterval(interval)
|
||||
}, [hasSession])
|
||||
|
||||
// listen for broadcasts
|
||||
React.useEffect(() => {
|
||||
const listener = ({data}: MessageEvent) => {
|
||||
@@ -177,21 +190,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
},
|
||||
}
|
||||
}, [setNumUnread, queryClient, moderationOpts, agent])
|
||||
|
||||
const checkUnread = useNonReactiveCallback(api.checkUnread)
|
||||
|
||||
// periodic sync
|
||||
React.useEffect(() => {
|
||||
if (!hasSession) {
|
||||
return
|
||||
}
|
||||
checkUnread() // fire on init
|
||||
const interval = setInterval(
|
||||
() => checkUnread({isPoll: true}),
|
||||
UPDATE_INTERVAL,
|
||||
)
|
||||
return () => clearInterval(interval)
|
||||
}, [hasSession, checkUnread])
|
||||
checkUnreadRef.current = api.checkUnread
|
||||
|
||||
return (
|
||||
<stateContext.Provider value={numUnread}>
|
||||
|
||||
@@ -216,17 +216,6 @@ export function sortThread(
|
||||
}
|
||||
}
|
||||
|
||||
const aPin = Boolean(a.record.text.trim() === '📌')
|
||||
const bPin = Boolean(b.record.text.trim() === '📌')
|
||||
if (aPin !== bPin) {
|
||||
if (aPin) {
|
||||
return 1
|
||||
}
|
||||
if (bPin) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.prioritizeFollowedUsers) {
|
||||
const af = a.post.author.viewer?.following
|
||||
const bf = b.post.author.viewer?.following
|
||||
|
||||
@@ -187,8 +187,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.needsPersist) {
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
state.needsPersist = false
|
||||
const persistedData = {
|
||||
accounts: state.accounts,
|
||||
|
||||
@@ -44,17 +44,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
'worklet'
|
||||
// Cancel any existing animation
|
||||
cancelAnimation(headerMode)
|
||||
headerMode.set(() =>
|
||||
withSpring(v ? 1 : 0, {
|
||||
overshootClamping: true,
|
||||
}),
|
||||
)
|
||||
headerMode.value = withSpring(v ? 1 : 0, {
|
||||
overshootClamping: true,
|
||||
})
|
||||
cancelAnimation(footerMode)
|
||||
footerMode.set(() =>
|
||||
withSpring(v ? 1 : 0, {
|
||||
overshootClamping: true,
|
||||
}),
|
||||
)
|
||||
footerMode.value = withSpring(v ? 1 : 0, {
|
||||
overshootClamping: true,
|
||||
})
|
||||
},
|
||||
[headerMode, footerMode],
|
||||
)
|
||||
|
||||
@@ -61,17 +61,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const {mutateAsync, variables, isPending} =
|
||||
useSetActiveProgressGuideMutation()
|
||||
|
||||
let activeProgressGuide = (
|
||||
const activeProgressGuide = (
|
||||
isPending ? variables : preferences?.bskyAppState?.activeProgressGuide
|
||||
) as ProgressGuide
|
||||
|
||||
// ensure the unspecced attributes have the correct types
|
||||
if (activeProgressGuide?.guide === 'like-10-and-follow-7') {
|
||||
activeProgressGuide = {
|
||||
...activeProgressGuide,
|
||||
numLikes: Number(activeProgressGuide.numLikes) || 0,
|
||||
numFollows: Number(activeProgressGuide.numFollows) || 0,
|
||||
}
|
||||
activeProgressGuide.numLikes = Number(activeProgressGuide.numLikes) || 0
|
||||
activeProgressGuide.numFollows = Number(activeProgressGuide.numFollows) || 0
|
||||
}
|
||||
|
||||
const [localGuideState, setLocalGuideState] =
|
||||
|
||||
@@ -130,7 +130,6 @@ import {
|
||||
ThreadDraft,
|
||||
} from './state/composer'
|
||||
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
|
||||
import {clearThumbnailCache} from './videos/VideoTranscodeBackdrop'
|
||||
|
||||
type CancelRef = {
|
||||
onPressCancel: () => void
|
||||
@@ -250,8 +249,7 @@ export const ComposePost = ({
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
closeComposer()
|
||||
clearThumbnailCache(queryClient)
|
||||
}, [closeComposer, queryClient])
|
||||
}, [closeComposer])
|
||||
|
||||
const insets = useSafeAreaInsets()
|
||||
const viewStyles = useMemo(
|
||||
@@ -1267,12 +1265,12 @@ function useScrollTracker({
|
||||
const contentHeight = useSharedValue(0)
|
||||
|
||||
const hasScrolledToTop = useDerivedValue(() =>
|
||||
withTiming(contentOffset.get() === 0 ? 1 : 0),
|
||||
withTiming(contentOffset.value === 0 ? 1 : 0),
|
||||
)
|
||||
|
||||
const hasScrolledToBottom = useDerivedValue(() =>
|
||||
withTiming(
|
||||
contentHeight.get() - contentOffset.get() - 5 <= scrollViewHeight.get()
|
||||
contentHeight.value - contentOffset.value - 5 <= scrollViewHeight.value
|
||||
? 1
|
||||
: 0,
|
||||
),
|
||||
@@ -1290,11 +1288,11 @@ function useScrollTracker({
|
||||
}) => {
|
||||
'worklet'
|
||||
if (typeof newContentHeight === 'number')
|
||||
contentHeight.set(Math.floor(newContentHeight))
|
||||
contentHeight.value = Math.floor(newContentHeight)
|
||||
if (typeof newContentOffset === 'number')
|
||||
contentOffset.set(Math.floor(newContentOffset))
|
||||
contentOffset.value = Math.floor(newContentOffset)
|
||||
if (typeof newScrollViewHeight === 'number')
|
||||
scrollViewHeight.set(Math.floor(newScrollViewHeight))
|
||||
scrollViewHeight.value = Math.floor(newScrollViewHeight)
|
||||
},
|
||||
[contentHeight, contentOffset, scrollViewHeight],
|
||||
)
|
||||
@@ -1310,22 +1308,21 @@ function useScrollTracker({
|
||||
},
|
||||
})
|
||||
|
||||
const onScrollViewContentSizeChangeUIThread = useCallback(
|
||||
(newContentHeight: number) => {
|
||||
'worklet'
|
||||
const oldContentHeight = contentHeight.get()
|
||||
let shouldScrollToBottom = false
|
||||
if (stickyBottom && newContentHeight > oldContentHeight) {
|
||||
const onScrollViewContentSizeChange = useCallback(
|
||||
(_width: number, height: number) => {
|
||||
if (stickyBottom && height > contentHeight.value) {
|
||||
const isFairlyCloseToBottom =
|
||||
oldContentHeight - contentOffset.get() - 100 <= scrollViewHeight.get()
|
||||
contentHeight.value - contentOffset.value - 100 <=
|
||||
scrollViewHeight.value
|
||||
if (isFairlyCloseToBottom) {
|
||||
shouldScrollToBottom = true
|
||||
runOnUI(() => {
|
||||
scrollTo(scrollViewRef, 0, contentHeight.value, true)
|
||||
})()
|
||||
}
|
||||
}
|
||||
showHideBottomBorder({newContentHeight})
|
||||
if (shouldScrollToBottom) {
|
||||
scrollTo(scrollViewRef, 0, newContentHeight, true)
|
||||
}
|
||||
showHideBottomBorder({
|
||||
newContentHeight: height,
|
||||
})
|
||||
},
|
||||
[
|
||||
showHideBottomBorder,
|
||||
@@ -1337,13 +1334,6 @@ function useScrollTracker({
|
||||
],
|
||||
)
|
||||
|
||||
const onScrollViewContentSizeChange = useCallback(
|
||||
(_width: number, height: number) => {
|
||||
runOnUI(onScrollViewContentSizeChangeUIThread)(height)
|
||||
},
|
||||
[onScrollViewContentSizeChangeUIThread],
|
||||
)
|
||||
|
||||
const onScrollViewLayout = useCallback(
|
||||
(evt: LayoutChangeEvent) => {
|
||||
showHideBottomBorder({
|
||||
@@ -1357,7 +1347,7 @@ function useScrollTracker({
|
||||
return {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: interpolateColor(
|
||||
hasScrolledToTop.get(),
|
||||
hasScrolledToTop.value,
|
||||
[0, 1],
|
||||
[t.atoms.border_contrast_medium.borderColor, 'transparent'],
|
||||
),
|
||||
@@ -1367,7 +1357,7 @@ function useScrollTracker({
|
||||
return {
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: interpolateColor(
|
||||
hasScrolledToBottom.get(),
|
||||
hasScrolledToBottom.value,
|
||||
[0, 1],
|
||||
[t.atoms.border_contrast_medium.borderColor, 'transparent'],
|
||||
),
|
||||
@@ -1612,7 +1602,7 @@ function VideoUploadToolbar({state}: {state: VideoState}) {
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [{rotateZ: `${rotate.get()}deg`}],
|
||||
transform: [{rotateZ: `${rotate.value}deg`}],
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -158,26 +158,22 @@ function DialogInner({
|
||||
<Toggle.Item name="porn" label={_(msg`Porn`)}>
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.LabelText>
|
||||
<Trans>Adult</Trans>
|
||||
<Trans>Porn</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
{labels.includes('sexual') ||
|
||||
labels.includes('nudity') ||
|
||||
labels.includes('porn') ? (
|
||||
<Text style={[a.mt_sm, t.atoms.text_contrast_medium]}>
|
||||
{labels.includes('sexual') ? (
|
||||
<Trans>Pictures meant for adults.</Trans>
|
||||
) : labels.includes('nudity') ? (
|
||||
<Trans>Artistic or non-erotic nudity.</Trans>
|
||||
) : labels.includes('porn') ? (
|
||||
<Trans>Sexual activity or erotic nudity.</Trans>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={[a.mt_sm, t.atoms.text_contrast_medium]}>
|
||||
{labels.includes('sexual') ? (
|
||||
<Trans>Pictures meant for adults.</Trans>
|
||||
) : labels.includes('nudity') ? (
|
||||
<Trans>Artistic or non-erotic nudity.</Trans>
|
||||
) : labels.includes('porn') ? (
|
||||
<Trans>Sexual activity or erotic nudity.</Trans>
|
||||
) : (
|
||||
<Trans>Does not contain adult content.</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
@@ -207,14 +203,16 @@ function DialogInner({
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Item>
|
||||
</Toggle.Group>
|
||||
{labels.includes('graphic-media') ? (
|
||||
<Text style={[a.mt_sm, t.atoms.text_contrast_medium]}>
|
||||
<Text style={[a.mt_sm, t.atoms.text_contrast_medium]}>
|
||||
{labels.includes('graphic-media') ? (
|
||||
<Trans>
|
||||
Media that may be disturbing or inappropriate for some
|
||||
audiences.
|
||||
</Trans>
|
||||
</Text>
|
||||
) : null}
|
||||
) : (
|
||||
<Trans>Does not contain graphic or disturbing content.</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -303,8 +303,6 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
React.useLayoutEffect(() => {
|
||||
let node = editor?.view.dom
|
||||
if (node) {
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
node.style.minHeight = webForceMinHeight ? '140px' : ''
|
||||
}
|
||||
}, [editor, webForceMinHeight])
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import React from 'react'
|
||||
import React, {useEffect} from 'react'
|
||||
import {clearCache, createVideoThumbnail} from 'react-native-compressor'
|
||||
import Animated, {FadeIn} from 'react-native-reanimated'
|
||||
import {Image} from 'expo-image'
|
||||
import {QueryClient, useQuery} from '@tanstack/react-query'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
export const RQKEY = 'video-thumbnail'
|
||||
|
||||
export function clearThumbnailCache(queryClient: QueryClient) {
|
||||
clearCache()
|
||||
queryClient.resetQueries({queryKey: [RQKEY]})
|
||||
}
|
||||
|
||||
export function VideoTranscodeBackdrop({uri}: {uri: string}) {
|
||||
const {data: thumbnail} = useQuery({
|
||||
queryKey: [RQKEY, uri],
|
||||
queryKey: ['thumbnail', uri],
|
||||
queryFn: async () => {
|
||||
return await createVideoThumbnail(uri)
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearCache()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
thumbnail && (
|
||||
<Animated.View style={a.flex_1} entering={FadeIn}>
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
export function clearThumbnailCache() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
export function VideoTranscodeBackdrop() {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ function HomeHeaderLayoutDesktopAndTablet({
|
||||
{tabBarAnchor}
|
||||
<Animated.View
|
||||
onLayout={e => {
|
||||
headerHeight.set(e.nativeEvent.layout.height)
|
||||
headerHeight.value = e.nativeEvent.layout.height
|
||||
}}
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
|
||||
@@ -43,7 +43,7 @@ export function HomeHeaderLayoutMobile({
|
||||
<Animated.View
|
||||
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
|
||||
onLayout={e => {
|
||||
headerHeight.set(e.nativeEvent.layout.height)
|
||||
headerHeight.value = e.nativeEvent.layout.height
|
||||
}}>
|
||||
<View style={[pal.view, styles.topBar]}>
|
||||
<View style={[pal.view, {width: 100}]}>
|
||||
|
||||
@@ -87,11 +87,11 @@ const ImageItem = ({
|
||||
// Note: DO NOT move any logic reading animated values outside this function.
|
||||
useAnimatedReaction(
|
||||
() => {
|
||||
if (pinchScale.get() !== 1) {
|
||||
if (pinchScale.value !== 1) {
|
||||
// We're currently pinching.
|
||||
return true
|
||||
}
|
||||
const [, , committedScale] = readTransform(committedTransform.get())
|
||||
const [, , committedScale] = readTransform(committedTransform.value)
|
||||
if (committedScale !== 1) {
|
||||
// We started from a pinched in state.
|
||||
return true
|
||||
@@ -147,10 +147,10 @@ const ImageItem = ({
|
||||
.onStart(e => {
|
||||
'worklet'
|
||||
const screenSize = measureSafeArea()
|
||||
pinchOrigin.set({
|
||||
pinchOrigin.value = {
|
||||
x: e.focalX - screenSize.width / 2,
|
||||
y: e.focalY - screenSize.height / 2,
|
||||
})
|
||||
}
|
||||
})
|
||||
.onChange(e => {
|
||||
'worklet'
|
||||
@@ -160,7 +160,7 @@ const ImageItem = ({
|
||||
}
|
||||
// Don't let the picture zoom in so close that it gets blurry.
|
||||
// Also, like in stock Android apps, don't let the user zoom out further than 1:1.
|
||||
const [, , committedScale] = readTransform(committedTransform.get())
|
||||
const [, , committedScale] = readTransform(committedTransform.value)
|
||||
const maxCommittedScale = Math.max(
|
||||
MIN_SCREEN_ZOOM,
|
||||
(imageDimensions.width / screenSize.width) * MAX_ORIGINAL_IMAGE_ZOOM,
|
||||
@@ -171,21 +171,20 @@ const ImageItem = ({
|
||||
Math.max(minPinchScale, e.scale),
|
||||
maxPinchScale,
|
||||
)
|
||||
pinchScale.set(nextPinchScale)
|
||||
pinchScale.value = nextPinchScale
|
||||
|
||||
// Zooming out close to the corner could push us out of bounds, which we don't want on Android.
|
||||
// Calculate where we'll end up so we know how much to translate back to stay in bounds.
|
||||
const t = createTransform()
|
||||
prependPan(t, panTranslation.get())
|
||||
prependPinch(t, nextPinchScale, pinchOrigin.get(), pinchTranslation.get())
|
||||
prependTransform(t, committedTransform.get())
|
||||
prependPan(t, panTranslation.value)
|
||||
prependPinch(t, nextPinchScale, pinchOrigin.value, pinchTranslation.value)
|
||||
prependTransform(t, committedTransform.value)
|
||||
const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize)
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
const pt = pinchTranslation.get()
|
||||
pinchTranslation.set({
|
||||
x: pt.x + dx,
|
||||
y: pt.y + dy,
|
||||
})
|
||||
pinchTranslation.value = {
|
||||
x: pinchTranslation.value.x + dx,
|
||||
y: pinchTranslation.value.y + dy,
|
||||
}
|
||||
}
|
||||
})
|
||||
.onEnd(() => {
|
||||
@@ -194,18 +193,18 @@ const ImageItem = ({
|
||||
let t = createTransform()
|
||||
prependPinch(
|
||||
t,
|
||||
pinchScale.get(),
|
||||
pinchOrigin.get(),
|
||||
pinchTranslation.get(),
|
||||
pinchScale.value,
|
||||
pinchOrigin.value,
|
||||
pinchTranslation.value,
|
||||
)
|
||||
prependTransform(t, committedTransform.get())
|
||||
prependTransform(t, committedTransform.value)
|
||||
applyRounding(t)
|
||||
committedTransform.set(t)
|
||||
committedTransform.value = t
|
||||
|
||||
// Reset just the pinch.
|
||||
pinchScale.set(1)
|
||||
pinchOrigin.set({x: 0, y: 0})
|
||||
pinchTranslation.set({x: 0, y: 0})
|
||||
pinchScale.value = 1
|
||||
pinchOrigin.value = {x: 0, y: 0}
|
||||
pinchTranslation.value = {x: 0, y: 0}
|
||||
})
|
||||
|
||||
const pan = Gesture.Pan()
|
||||
@@ -224,29 +223,29 @@ const ImageItem = ({
|
||||
prependPan(t, nextPanTranslation)
|
||||
prependPinch(
|
||||
t,
|
||||
pinchScale.get(),
|
||||
pinchOrigin.get(),
|
||||
pinchTranslation.get(),
|
||||
pinchScale.value,
|
||||
pinchOrigin.value,
|
||||
pinchTranslation.value,
|
||||
)
|
||||
prependTransform(t, committedTransform.get())
|
||||
prependTransform(t, committedTransform.value)
|
||||
|
||||
// Prevent panning from going out of bounds.
|
||||
const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize)
|
||||
nextPanTranslation.x += dx
|
||||
nextPanTranslation.y += dy
|
||||
panTranslation.set(nextPanTranslation)
|
||||
panTranslation.value = nextPanTranslation
|
||||
})
|
||||
.onEnd(() => {
|
||||
'worklet'
|
||||
// Commit just the pan.
|
||||
let t = createTransform()
|
||||
prependPan(t, panTranslation.get())
|
||||
prependTransform(t, committedTransform.get())
|
||||
prependPan(t, panTranslation.value)
|
||||
prependTransform(t, committedTransform.value)
|
||||
applyRounding(t)
|
||||
committedTransform.set(t)
|
||||
committedTransform.value = t
|
||||
|
||||
// Reset just the pan.
|
||||
panTranslation.set({x: 0, y: 0})
|
||||
panTranslation.value = {x: 0, y: 0}
|
||||
})
|
||||
|
||||
const singleTap = Gesture.Tap().onEnd(() => {
|
||||
@@ -262,11 +261,11 @@ const ImageItem = ({
|
||||
if (!imageDimensions || !imageAspect) {
|
||||
return
|
||||
}
|
||||
const [, , committedScale] = readTransform(committedTransform.get())
|
||||
const [, , committedScale] = readTransform(committedTransform.value)
|
||||
if (committedScale !== 1) {
|
||||
// Go back to 1:1 using the identity vector.
|
||||
let t = createTransform()
|
||||
committedTransform.set(withClampedSpring(t))
|
||||
committedTransform.value = withClampedSpring(t)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -300,7 +299,7 @@ const ImageItem = ({
|
||||
)
|
||||
const finalTransform = createTransform()
|
||||
prependPinch(finalTransform, scale, origin, {x: dx, y: dy})
|
||||
committedTransform.set(withClampedSpring(finalTransform))
|
||||
committedTransform.value = withClampedSpring(finalTransform)
|
||||
})
|
||||
|
||||
const composedGesture = isScrollViewBeingDragged
|
||||
@@ -314,13 +313,13 @@ const ImageItem = ({
|
||||
)
|
||||
|
||||
const containerStyle = useAnimatedStyle(() => {
|
||||
const {scaleAndMoveTransform, isHidden} = transforms.get()
|
||||
const {scaleAndMoveTransform, isHidden} = transforms.value
|
||||
// Apply the active adjustments on top of the committed transform before the gestures.
|
||||
// This is matrix multiplication, so operations are applied in the reverse order.
|
||||
let t = createTransform()
|
||||
prependPan(t, panTranslation.get())
|
||||
prependPinch(t, pinchScale.get(), pinchOrigin.get(), pinchTranslation.get())
|
||||
prependTransform(t, committedTransform.get())
|
||||
prependPan(t, panTranslation.value)
|
||||
prependPinch(t, pinchScale.value, pinchOrigin.value, pinchTranslation.value)
|
||||
prependTransform(t, committedTransform.value)
|
||||
const [translateX, translateY, scale] = readTransform(t)
|
||||
const manipulationTransform = [
|
||||
{translateX},
|
||||
@@ -339,7 +338,7 @@ const ImageItem = ({
|
||||
})
|
||||
|
||||
const imageCropStyle = useAnimatedStyle(() => {
|
||||
const {cropFrameTransform} = transforms.get()
|
||||
const {cropFrameTransform} = transforms.value
|
||||
return {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
@@ -348,7 +347,7 @@ const ImageItem = ({
|
||||
})
|
||||
|
||||
const imageStyle = useAnimatedStyle(() => {
|
||||
const {cropContentTransform} = transforms.get()
|
||||
const {cropContentTransform} = transforms.value
|
||||
return {
|
||||
flex: 1,
|
||||
transform: cropContentTransform,
|
||||
@@ -360,7 +359,7 @@ const ImageItem = ({
|
||||
const [hasLoaded, setHasLoaded] = useState(false)
|
||||
useAnimatedReaction(
|
||||
() => {
|
||||
return transforms.get().isResting && !hasLoaded
|
||||
return transforms.value.isResting && !hasLoaded
|
||||
},
|
||||
(show, prevShow) => {
|
||||
if (show && !prevShow) {
|
||||
|
||||
@@ -148,7 +148,7 @@ const ImageItem = ({
|
||||
)
|
||||
|
||||
const containerStyle = useAnimatedStyle(() => {
|
||||
const {scaleAndMoveTransform, isHidden} = transforms.get()
|
||||
const {scaleAndMoveTransform, isHidden} = transforms.value
|
||||
return {
|
||||
flex: 1,
|
||||
transform: scaleAndMoveTransform,
|
||||
@@ -158,7 +158,7 @@ const ImageItem = ({
|
||||
|
||||
const imageCropStyle = useAnimatedStyle(() => {
|
||||
const screenSize = measureSafeArea()
|
||||
const {cropFrameTransform} = transforms.get()
|
||||
const {cropFrameTransform} = transforms.value
|
||||
return {
|
||||
overflow: 'hidden',
|
||||
transform: cropFrameTransform,
|
||||
@@ -171,7 +171,7 @@ const ImageItem = ({
|
||||
})
|
||||
|
||||
const imageStyle = useAnimatedStyle(() => {
|
||||
const {cropContentTransform} = transforms.get()
|
||||
const {cropContentTransform} = transforms.value
|
||||
return {
|
||||
transform: cropContentTransform,
|
||||
width: '100%',
|
||||
@@ -184,7 +184,7 @@ const ImageItem = ({
|
||||
const [hasLoaded, setHasLoaded] = useState(false)
|
||||
useAnimatedReaction(
|
||||
() => {
|
||||
return transforms.get().isResting && !hasLoaded
|
||||
return transforms.value.isResting && !hasLoaded
|
||||
},
|
||||
(show, prevShow) => {
|
||||
if (show && !prevShow) {
|
||||
|
||||
@@ -32,7 +32,6 @@ import Animated, {
|
||||
useSharedValue,
|
||||
withDecay,
|
||||
withSpring,
|
||||
WithSpringConfig,
|
||||
} from 'react-native-reanimated'
|
||||
import {
|
||||
Edge,
|
||||
@@ -63,18 +62,8 @@ const EDGES =
|
||||
? (['top', 'bottom', 'left', 'right'] satisfies Edge[])
|
||||
: (['left', 'right'] satisfies Edge[]) // iOS, so no top/bottom safe area
|
||||
|
||||
const SLOW_SPRING: WithSpringConfig = {
|
||||
mass: isIOS ? 1.25 : 0.75,
|
||||
damping: 300,
|
||||
stiffness: 800,
|
||||
restDisplacementThreshold: 0.01,
|
||||
}
|
||||
const FAST_SPRING: WithSpringConfig = {
|
||||
mass: isIOS ? 1.25 : 0.75,
|
||||
damping: 150,
|
||||
stiffness: 900,
|
||||
restDisplacementThreshold: 0.01,
|
||||
}
|
||||
const SLOW_SPRING = {stiffness: 120}
|
||||
const FAST_SPRING = {stiffness: 700}
|
||||
|
||||
export default function ImageViewRoot({
|
||||
lightbox: nextLightbox,
|
||||
@@ -87,6 +76,7 @@ export default function ImageViewRoot({
|
||||
onPressSave: (uri: string) => void
|
||||
onPressShare: (uri: string) => void
|
||||
}) {
|
||||
'use no memo'
|
||||
const ref = useAnimatedRef<View>()
|
||||
const [activeLightbox, setActiveLightbox] = useState(nextLightbox)
|
||||
const openProgress = useSharedValue(0)
|
||||
@@ -108,22 +98,18 @@ export default function ImageViewRoot({
|
||||
|
||||
// https://github.com/software-mansion/react-native-reanimated/issues/6677
|
||||
requestAnimationFrame(() => {
|
||||
openProgress.set(() =>
|
||||
canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1,
|
||||
)
|
||||
openProgress.value = canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1
|
||||
})
|
||||
return () => {
|
||||
// https://github.com/software-mansion/react-native-reanimated/issues/6677
|
||||
requestAnimationFrame(() => {
|
||||
openProgress.set(() =>
|
||||
canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0,
|
||||
)
|
||||
openProgress.value = canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0
|
||||
})
|
||||
}
|
||||
}, [nextLightbox, openProgress])
|
||||
|
||||
useAnimatedReaction(
|
||||
() => openProgress.get() === 0,
|
||||
() => openProgress.value === 0,
|
||||
(isGone, wasGone) => {
|
||||
if (isGone && !wasGone) {
|
||||
runOnJS(setActiveLightbox)(null)
|
||||
@@ -133,7 +119,7 @@ export default function ImageViewRoot({
|
||||
|
||||
const onFlyAway = React.useCallback(() => {
|
||||
'worklet'
|
||||
openProgress.set(0)
|
||||
openProgress.value = 0
|
||||
runOnJS(onRequestClose)()
|
||||
}, [onRequestClose, openProgress])
|
||||
|
||||
@@ -190,7 +176,7 @@ function ImageView({
|
||||
const isFlyingAway = useSharedValue(false)
|
||||
|
||||
const containerStyle = useAnimatedStyle(() => {
|
||||
if (openProgress.get() < 1 || isFlyingAway.get()) {
|
||||
if (openProgress.value < 1 || isFlyingAway.value) {
|
||||
return {pointerEvents: 'none'}
|
||||
}
|
||||
return {pointerEvents: 'auto'}
|
||||
@@ -199,12 +185,11 @@ function ImageView({
|
||||
const backdropStyle = useAnimatedStyle(() => {
|
||||
const screenSize = measure(safeAreaRef)
|
||||
let opacity = 1
|
||||
const openProgressValue = openProgress.get()
|
||||
if (openProgressValue < 1) {
|
||||
opacity = Math.sqrt(openProgressValue)
|
||||
if (openProgress.value < 1) {
|
||||
opacity = Math.sqrt(openProgress.value)
|
||||
} else if (screenSize) {
|
||||
const dragProgress = Math.min(
|
||||
Math.abs(dismissSwipeTranslateY.get()) / (screenSize.height / 2),
|
||||
Math.abs(dismissSwipeTranslateY.value) / (screenSize.height / 2),
|
||||
1,
|
||||
)
|
||||
opacity -= dragProgress
|
||||
@@ -216,11 +201,11 @@ function ImageView({
|
||||
})
|
||||
|
||||
const animatedHeaderStyle = useAnimatedStyle(() => {
|
||||
const show = showControls && dismissSwipeTranslateY.get() === 0
|
||||
const show = showControls && dismissSwipeTranslateY.value === 0
|
||||
return {
|
||||
pointerEvents: show ? 'box-none' : 'none',
|
||||
opacity: withClampedSpring(
|
||||
show && openProgress.get() === 1 ? 1 : 0,
|
||||
show && openProgress.value === 1 ? 1 : 0,
|
||||
FAST_SPRING,
|
||||
),
|
||||
transform: [
|
||||
@@ -231,12 +216,12 @@ function ImageView({
|
||||
}
|
||||
})
|
||||
const animatedFooterStyle = useAnimatedStyle(() => {
|
||||
const show = showControls && dismissSwipeTranslateY.get() === 0
|
||||
const show = showControls && dismissSwipeTranslateY.value === 0
|
||||
return {
|
||||
flexGrow: 1,
|
||||
pointerEvents: show ? 'box-none' : 'none',
|
||||
opacity: withClampedSpring(
|
||||
show && openProgress.get() === 1 ? 1 : 0,
|
||||
show && openProgress.value === 1 ? 1 : 0,
|
||||
FAST_SPRING,
|
||||
),
|
||||
transform: [
|
||||
@@ -263,7 +248,7 @@ function ImageView({
|
||||
const screenSize = measure(safeAreaRef)
|
||||
return (
|
||||
!screenSize ||
|
||||
Math.abs(dismissSwipeTranslateY.get()) > screenSize.height
|
||||
Math.abs(dismissSwipeTranslateY.value) > screenSize.height
|
||||
)
|
||||
},
|
||||
(isOut, wasOut) => {
|
||||
@@ -401,11 +386,10 @@ function LightboxImage({
|
||||
const transforms = useDerivedValue(() => {
|
||||
'worklet'
|
||||
const safeArea = measureSafeArea()
|
||||
const openProgressValue = openProgress.get()
|
||||
const dismissTranslateY =
|
||||
isActive && openProgressValue === 1 ? dismissSwipeTranslateY.get() : 0
|
||||
isActive && openProgress.value === 1 ? dismissSwipeTranslateY.value : 0
|
||||
|
||||
if (openProgressValue === 0 && isFlyingAway.get()) {
|
||||
if (openProgress.value === 0 && isFlyingAway.value) {
|
||||
return {
|
||||
isHidden: true,
|
||||
isResting: false,
|
||||
@@ -415,9 +399,9 @@ function LightboxImage({
|
||||
}
|
||||
}
|
||||
|
||||
if (isActive && thumbRect && imageAspect && openProgressValue < 1) {
|
||||
if (isActive && thumbRect && imageAspect && openProgress.value < 1) {
|
||||
return interpolateTransform(
|
||||
openProgressValue,
|
||||
openProgress.value,
|
||||
thumbRect,
|
||||
safeArea,
|
||||
imageAspect,
|
||||
@@ -439,37 +423,33 @@ function LightboxImage({
|
||||
.maxPointers(1)
|
||||
.onUpdate(e => {
|
||||
'worklet'
|
||||
if (openProgress.get() !== 1 || isFlyingAway.get()) {
|
||||
if (openProgress.value !== 1 || isFlyingAway.value) {
|
||||
return
|
||||
}
|
||||
dismissSwipeTranslateY.set(e.translationY)
|
||||
dismissSwipeTranslateY.value = e.translationY
|
||||
})
|
||||
.onEnd(e => {
|
||||
'worklet'
|
||||
if (openProgress.get() !== 1 || isFlyingAway.get()) {
|
||||
if (openProgress.value !== 1 || isFlyingAway.value) {
|
||||
return
|
||||
}
|
||||
if (Math.abs(e.velocityY) > 200) {
|
||||
isFlyingAway.set(true)
|
||||
if (dismissSwipeTranslateY.get() === 0) {
|
||||
if (Math.abs(e.velocityY) > 1000) {
|
||||
isFlyingAway.value = true
|
||||
if (dismissSwipeTranslateY.value === 0) {
|
||||
// HACK: If the initial value is 0, withDecay() animation doesn't start.
|
||||
// This is a bug in Reanimated, but for now we'll work around it like this.
|
||||
dismissSwipeTranslateY.set(1)
|
||||
dismissSwipeTranslateY.value = 1
|
||||
}
|
||||
dismissSwipeTranslateY.set(() =>
|
||||
withDecay({
|
||||
velocity: e.velocityY,
|
||||
velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
|
||||
deceleration: 1, // Danger! This relies on the reaction below stopping it.
|
||||
}),
|
||||
)
|
||||
dismissSwipeTranslateY.value = withDecay({
|
||||
velocity: e.velocityY,
|
||||
velocityFactor: Math.max(3000 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
|
||||
deceleration: 1, // Danger! This relies on the reaction below stopping it.
|
||||
})
|
||||
} else {
|
||||
dismissSwipeTranslateY.set(() =>
|
||||
withSpring(0, {
|
||||
stiffness: 700,
|
||||
damping: 50,
|
||||
}),
|
||||
)
|
||||
dismissSwipeTranslateY.value = withSpring(0, {
|
||||
stiffness: 700,
|
||||
damping: 50,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -726,7 +706,7 @@ function interpolateTransform(
|
||||
}
|
||||
}
|
||||
|
||||
function withClampedSpring(value: any, config: WithSpringConfig) {
|
||||
function withClampedSpring(value: any, {stiffness}: {stiffness: number}) {
|
||||
'worklet'
|
||||
return withSpring(value, {...config, overshootClamping: true})
|
||||
return withSpring(value, {overshootClamping: true, stiffness})
|
||||
}
|
||||
|
||||
@@ -131,11 +131,11 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||
const lastForcedScrollY = useSharedValue(0)
|
||||
const adjustScrollForOtherPages = () => {
|
||||
'worklet'
|
||||
const currentScrollY = scrollY.get()
|
||||
const currentScrollY = scrollY.value
|
||||
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
|
||||
if (lastForcedScrollY.get() !== forcedScrollY) {
|
||||
lastForcedScrollY.set(forcedScrollY)
|
||||
const refs = scrollRefs.get()
|
||||
if (lastForcedScrollY.value !== forcedScrollY) {
|
||||
lastForcedScrollY.value = forcedScrollY
|
||||
const refs = scrollRefs.value
|
||||
for (let i = 0; i < refs.length; i++) {
|
||||
const scollRef = refs[i]
|
||||
if (i !== currentPage && scollRef != null) {
|
||||
@@ -167,7 +167,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||
const isPossiblyInvalid =
|
||||
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
|
||||
if (!isPossiblyInvalid) {
|
||||
scrollY.set(nextScrollY)
|
||||
scrollY.value = nextScrollY
|
||||
runOnJS(queueThrottledOnScroll)()
|
||||
}
|
||||
},
|
||||
@@ -246,7 +246,7 @@ let PagerTabBar = ({
|
||||
allowHeaderOverScroll?: boolean
|
||||
}): React.ReactNode => {
|
||||
const headerTransform = useAnimatedStyle(() => {
|
||||
const translateY = Math.min(scrollY.get(), headerOnlyHeight) * -1
|
||||
const translateY = Math.min(scrollY.value, headerOnlyHeight) * -1
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {memo, useMemo} from 'react'
|
||||
import {StyleSheet, Text as RNText, View} from 'react-native'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ModerationDecision,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -20,7 +21,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
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 {useLanguagePrefs} from '#/state/preferences'
|
||||
import {ThreadPost} from '#/state/queries/post-thread'
|
||||
@@ -28,31 +28,26 @@ import {useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {Link, TextLink} from '#/view/com/util/Link'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
|
||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {colors} from '#/components/Admonition'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
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 * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {WhoCanReply} from '#/components/WhoCanReply'
|
||||
import {Text as NewText} from '#/components/Typography'
|
||||
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 {WhoCanReply} from '../../../components/WhoCanReply'
|
||||
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {Link, TextLink} from '../util/Link'
|
||||
import {formatCount} from '../util/numeric/format'
|
||||
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
||||
import {PostEmbeds, PostEmbedViewContext} from '../util/post-embeds'
|
||||
import {PostMeta} from '../util/PostMeta'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
|
||||
export function PostThreadItem({
|
||||
post,
|
||||
@@ -130,20 +125,19 @@ export function PostThreadItem({
|
||||
}
|
||||
|
||||
function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) {
|
||||
const t = useTheme()
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
a.p_xl,
|
||||
a.pl_lg,
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
!hideTopBorder && a.border_t,
|
||||
styles.outer,
|
||||
pal.border,
|
||||
pal.view,
|
||||
s.p20,
|
||||
s.flexRow,
|
||||
hideTopBorder && styles.noTopBorder,
|
||||
]}>
|
||||
<TrashIcon style={[t.atoms.text]} />
|
||||
<Text style={[t.atoms.text_contrast_medium, a.mt_2xs]}>
|
||||
<FontAwesomeIcon icon={['far', 'trash-can']} color={pal.colors.icon} />
|
||||
<Text style={[pal.textLight, s.ml10]}>
|
||||
<Trans>This post has been deleted.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
@@ -314,7 +308,7 @@ let PostThreadItemLoaded = ({
|
||||
/>
|
||||
<View style={[a.flex_1]}>
|
||||
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
||||
<Text
|
||||
<NewText
|
||||
emoji
|
||||
style={[a.text_lg, a.font_bold, a.leading_snug, a.self_start]}
|
||||
numberOfLines={1}>
|
||||
@@ -323,10 +317,10 @@ let PostThreadItemLoaded = ({
|
||||
sanitizeHandle(post.author.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
</Text>
|
||||
</NewText>
|
||||
</Link>
|
||||
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
||||
<Text
|
||||
<NewText
|
||||
emoji
|
||||
style={[
|
||||
a.text_md,
|
||||
@@ -335,7 +329,7 @@ let PostThreadItemLoaded = ({
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeHandle(post.author.handle, '@')}
|
||||
</Text>
|
||||
</NewText>
|
||||
</Link>
|
||||
</View>
|
||||
{currentAccount?.did !== post.author.did && (
|
||||
@@ -399,48 +393,48 @@ let PostThreadItemLoaded = ({
|
||||
]}>
|
||||
{post.repostCount != null && post.repostCount !== 0 ? (
|
||||
<Link href={repostsHref} title={repostsTitle}>
|
||||
<Text
|
||||
<NewText
|
||||
testID="repostCount-expanded"
|
||||
style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
<NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{formatCount(i18n, post.repostCount)}
|
||||
</Text>{' '}
|
||||
</NewText>{' '}
|
||||
<Plural
|
||||
value={post.repostCount}
|
||||
one="repost"
|
||||
other="reposts"
|
||||
/>
|
||||
</Text>
|
||||
</NewText>
|
||||
</Link>
|
||||
) : null}
|
||||
{post.quoteCount != null &&
|
||||
post.quoteCount !== 0 &&
|
||||
!post.viewer?.embeddingDisabled ? (
|
||||
<Link href={quotesHref} title={quotesTitle}>
|
||||
<Text
|
||||
<NewText
|
||||
testID="quoteCount-expanded"
|
||||
style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
<NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{formatCount(i18n, post.quoteCount)}
|
||||
</Text>{' '}
|
||||
</NewText>{' '}
|
||||
<Plural
|
||||
value={post.quoteCount}
|
||||
one="quote"
|
||||
other="quotes"
|
||||
/>
|
||||
</Text>
|
||||
</NewText>
|
||||
</Link>
|
||||
) : null}
|
||||
{post.likeCount != null && post.likeCount !== 0 ? (
|
||||
<Link href={likesHref} title={likesTitle}>
|
||||
<Text
|
||||
<NewText
|
||||
testID="likeCount-expanded"
|
||||
style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
<NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{formatCount(i18n, post.likeCount)}
|
||||
</Text>{' '}
|
||||
</NewText>{' '}
|
||||
<Plural value={post.likeCount} one="like" other="likes" />
|
||||
</Text>
|
||||
</NewText>
|
||||
</Link>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -623,13 +617,13 @@ let PostThreadItemLoaded = ({
|
||||
href={postHref}
|
||||
title={itemTitle}
|
||||
noFeedback>
|
||||
<Text
|
||||
style={[t.atoms.text_contrast_medium, a.font_bold, a.text_sm]}>
|
||||
<Text type="sm-medium" style={pal.textLight}>
|
||||
<Trans>More</Trans>
|
||||
</Text>
|
||||
<ChevronRightIcon
|
||||
size="xs"
|
||||
style={[t.atoms.text_contrast_medium]}
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
color={pal.colors.textLight}
|
||||
size={14}
|
||||
/>
|
||||
</Link>
|
||||
) : undefined}
|
||||
@@ -657,24 +651,26 @@ function PostOuterWrapper({
|
||||
hideTopBorder?: boolean
|
||||
}>) {
|
||||
const t = useTheme()
|
||||
const {
|
||||
state: hover,
|
||||
onIn: onHoverIn,
|
||||
onOut: onHoverOut,
|
||||
} = useInteractionState()
|
||||
const [hover, setHover] = React.useState(false)
|
||||
if (treeView && depth > 0) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.px_sm,
|
||||
a.flex_row,
|
||||
t.atoms.border_contrast_low,
|
||||
styles.cursor,
|
||||
depth === 1 && a.border_t,
|
||||
{
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: depth === 1 ? a.border_t.borderTopWidth : 0,
|
||||
},
|
||||
]}
|
||||
onPointerEnter={onHoverIn}
|
||||
onPointerLeave={onHoverOut}>
|
||||
onPointerEnter={() => {
|
||||
setHover(true)
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
setHover(false)
|
||||
}}>
|
||||
{Array.from(Array(depth - 1)).map((_, n: number) => (
|
||||
<View
|
||||
key={`${post.uri}-padding-${n}`}
|
||||
@@ -688,23 +684,18 @@ function PostOuterWrapper({
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
<View style={a.flex_1}>
|
||||
<SubtleWebHover
|
||||
hover={hover}
|
||||
style={{
|
||||
left: (depth === 1 ? 0 : 2) - a.pl_sm.paddingLeft,
|
||||
right: -a.pr_sm.paddingRight,
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</View>
|
||||
<View style={{flex: 1}}>{children}</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View
|
||||
onPointerEnter={onHoverIn}
|
||||
onPointerLeave={onHoverOut}
|
||||
onPointerEnter={() => {
|
||||
setHover(true)
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
setHover(false)
|
||||
}}
|
||||
style={[
|
||||
a.border_t,
|
||||
a.px_sm,
|
||||
@@ -737,128 +728,36 @@ function ExpandedPostDetails({
|
||||
const isRootPost = !('reply' in post.record)
|
||||
|
||||
const onTranslatePress = React.useCallback(() => {
|
||||
openLink(translatorUrl, true)
|
||||
openLink(translatorUrl)
|
||||
}, [openLink, translatorUrl])
|
||||
|
||||
return (
|
||||
<View style={[a.gap_md, a.pt_md, a.align_start]}>
|
||||
<BackdatedPostIndicator post={post} />
|
||||
<View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{niceDate(i18n, post.indexedAt)}
|
||||
</Text>
|
||||
{isRootPost && (
|
||||
<WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
|
||||
)}
|
||||
{needsTranslation && (
|
||||
<>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
·
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm, a.pt_md]}>
|
||||
<NewText style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{niceDate(i18n, post.indexedAt)}
|
||||
</NewText>
|
||||
{isRootPost && (
|
||||
<WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
|
||||
)}
|
||||
{needsTranslation && (
|
||||
<>
|
||||
<NewText style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
·
|
||||
</NewText>
|
||||
|
||||
<InlineLinkText
|
||||
to="#"
|
||||
label={_(msg`Translate`)}
|
||||
style={[a.text_sm, pal.link]}
|
||||
onPress={onTranslatePress}>
|
||||
<Trans>Translate</Trans>
|
||||
</InlineLinkText>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<InlineLinkText
|
||||
to="#"
|
||||
label={_(msg`Translate`)}
|
||||
style={[a.text_sm, pal.link]}
|
||||
onPress={onTranslatePress}>
|
||||
<Trans>Translate</Trans>
|
||||
</InlineLinkText>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const control = Prompt.usePromptControl()
|
||||
|
||||
const indexedAt = new Date(post.indexedAt)
|
||||
const createdAt = AppBskyFeedPost.isRecord(post.record)
|
||||
? new Date(post.record.createdAt)
|
||||
: new Date(post.indexedAt)
|
||||
|
||||
// backdated if createdAt is 24 hours or more before indexedAt
|
||||
const isBackdated =
|
||||
indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000
|
||||
|
||||
if (!isBackdated) return null
|
||||
|
||||
const orange = t.name === 'light' ? colors.warning.dark : colors.warning.light
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Archived post`)}
|
||||
accessibilityHint={_(
|
||||
msg`Show information about when this post was created`,
|
||||
)}
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
control.open()
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
(hovered || pressed) && t.atoms.bg_contrast_50,
|
||||
{
|
||||
gap: 3,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
},
|
||||
]}>
|
||||
<CalendarClockIcon fill={orange} size="sm" aria-hidden />
|
||||
<Text
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Archived from {niceDate(i18n, createdAt)}</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Prompt.Outer control={control}>
|
||||
<Prompt.TitleText>
|
||||
<Trans>Archived post</Trans>
|
||||
</Prompt.TitleText>
|
||||
<Prompt.DescriptionText>
|
||||
<Trans>
|
||||
This post claims to have been created on{' '}
|
||||
<RNText style={[a.font_bold]}>{niceDate(i18n, createdAt)}</RNText>,
|
||||
but was first seen by Bluesky on{' '}
|
||||
<RNText style={[a.font_bold]}>{niceDate(i18n, indexedAt)}</RNText>.
|
||||
</Trans>
|
||||
</Prompt.DescriptionText>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_high,
|
||||
a.pb_xl,
|
||||
]}>
|
||||
<Trans>
|
||||
Bluesky cannot confirm the authenticity of the claimed date.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Prompt.Actions>
|
||||
<Prompt.Action cta={_(msg`Okay`)} onPress={() => {}} />
|
||||
</Prompt.Actions>
|
||||
</Prompt.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function getThreadAuthor(
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
record: AppBskyFeedPost.Record,
|
||||
|
||||
@@ -18,7 +18,7 @@ export function createCustomBackdrop(
|
||||
// animated variables
|
||||
const opacity = useAnimatedStyle(() => ({
|
||||
opacity: interpolate(
|
||||
animatedIndex.get(), // current snap index
|
||||
animatedIndex.value, // current snap index
|
||||
[-1, 0], // input range
|
||||
[0, 0.5], // output range
|
||||
Extrapolation.CLAMP,
|
||||
|
||||
+60
-80
@@ -18,7 +18,6 @@ import {
|
||||
useNavigationDeduped,
|
||||
} from '#/lib/hooks/useNavigationDeduped'
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {getTabState, TabState} from '#/lib/routes/helpers'
|
||||
import {
|
||||
convertBskyAppUrlIfNeeded,
|
||||
isExternalUrl,
|
||||
@@ -26,7 +25,6 @@ import {
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import {TypographyVariant} from '#/lib/ThemeContext'
|
||||
import {isAndroid, isWeb} from '#/platform/detection'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {WebAuxClickWrapper} from '#/view/com/util/WebAuxClickWrapper'
|
||||
import {useTheme} from '#/alf'
|
||||
@@ -129,15 +127,15 @@ export const Link = memo(function Link({
|
||||
)
|
||||
}
|
||||
|
||||
let dataSet = props.dataSet
|
||||
if (anchorNoUnderline) {
|
||||
// @ts-ignore web only -prf
|
||||
dataSet = {...dataSet, noUnderline: 1}
|
||||
props.dataSet = props.dataSet || {}
|
||||
// @ts-ignore web only -prf
|
||||
props.dataSet.noUnderline = 1
|
||||
}
|
||||
|
||||
let accessibilityLabel = props.accessibilityLabel
|
||||
if (title && !accessibilityLabel) {
|
||||
accessibilityLabel = title
|
||||
if (title && !props.accessibilityLabel) {
|
||||
props.accessibilityLabel = title
|
||||
}
|
||||
|
||||
const Com = props.hoverStyle ? PressableWithHover : Pressable
|
||||
@@ -150,11 +148,7 @@ export const Link = memo(function Link({
|
||||
accessibilityRole="link"
|
||||
// @ts-ignore web only -prf
|
||||
href={anchorHref}
|
||||
{...props}
|
||||
// @ts-ignore web only
|
||||
dataSet={dataSet}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityHint={props.accessibilityHint}>
|
||||
{...props}>
|
||||
{children ? children : <Text>{title || 'link'}</Text>}
|
||||
</Com>
|
||||
)
|
||||
@@ -168,7 +162,7 @@ export const TextLink = memo(function TextLink({
|
||||
text,
|
||||
numberOfLines,
|
||||
lineHeight,
|
||||
dataSet: rawDataSet,
|
||||
dataSet,
|
||||
title,
|
||||
onPress,
|
||||
onBeforePress,
|
||||
@@ -191,7 +185,7 @@ export const TextLink = memo(function TextLink({
|
||||
anchorNoUnderline?: boolean
|
||||
onBeforePress?: () => void
|
||||
} & TextProps) {
|
||||
let {...props} = useLinkProps({to: sanitizeUrl(href)})
|
||||
const {...props} = useLinkProps({to: sanitizeUrl(href)})
|
||||
const navigation = useNavigationDeduped()
|
||||
const {openModal, closeModal} = useModalControls()
|
||||
const openLink = useOpenLink()
|
||||
@@ -200,68 +194,61 @@ export const TextLink = memo(function TextLink({
|
||||
console.error('Unable to detect mismatching label')
|
||||
}
|
||||
|
||||
let dataSet = rawDataSet
|
||||
if (anchorNoUnderline) {
|
||||
dataSet = {
|
||||
...dataSet,
|
||||
noUnderline: 1,
|
||||
}
|
||||
dataSet = dataSet ?? {}
|
||||
dataSet.noUnderline = 1
|
||||
}
|
||||
|
||||
props = {
|
||||
...props,
|
||||
onPress: React.useCallback(
|
||||
(e?: Event) => {
|
||||
const requiresWarning =
|
||||
!disableMismatchWarning &&
|
||||
linkRequiresWarning(href, typeof text === 'string' ? text : '')
|
||||
if (requiresWarning) {
|
||||
e?.preventDefault?.()
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: typeof text === 'string' ? text : '',
|
||||
href,
|
||||
})
|
||||
}
|
||||
if (
|
||||
isWeb &&
|
||||
href !== '#' &&
|
||||
e != null &&
|
||||
isModifiedEvent(e as React.MouseEvent)
|
||||
) {
|
||||
// Let the browser handle opening in new tab etc.
|
||||
return
|
||||
}
|
||||
onBeforePress?.()
|
||||
if (onPress) {
|
||||
e?.preventDefault?.()
|
||||
// @ts-ignore function signature differs by platform -prf
|
||||
return onPress()
|
||||
}
|
||||
return onPressInner(
|
||||
closeModal,
|
||||
navigation,
|
||||
sanitizeUrl(href),
|
||||
navigationAction,
|
||||
openLink,
|
||||
e,
|
||||
)
|
||||
},
|
||||
[
|
||||
onBeforePress,
|
||||
onPress,
|
||||
props.onPress = React.useCallback(
|
||||
(e?: Event) => {
|
||||
const requiresWarning =
|
||||
!disableMismatchWarning &&
|
||||
linkRequiresWarning(href, typeof text === 'string' ? text : '')
|
||||
if (requiresWarning) {
|
||||
e?.preventDefault?.()
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: typeof text === 'string' ? text : '',
|
||||
href,
|
||||
})
|
||||
}
|
||||
if (
|
||||
isWeb &&
|
||||
href !== '#' &&
|
||||
e != null &&
|
||||
isModifiedEvent(e as React.MouseEvent)
|
||||
) {
|
||||
// Let the browser handle opening in new tab etc.
|
||||
return
|
||||
}
|
||||
onBeforePress?.()
|
||||
if (onPress) {
|
||||
e?.preventDefault?.()
|
||||
// @ts-ignore function signature differs by platform -prf
|
||||
return onPress()
|
||||
}
|
||||
return onPressInner(
|
||||
closeModal,
|
||||
openModal,
|
||||
navigation,
|
||||
href,
|
||||
text,
|
||||
disableMismatchWarning,
|
||||
sanitizeUrl(href),
|
||||
navigationAction,
|
||||
openLink,
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
e,
|
||||
)
|
||||
},
|
||||
[
|
||||
onBeforePress,
|
||||
onPress,
|
||||
closeModal,
|
||||
openModal,
|
||||
navigation,
|
||||
href,
|
||||
text,
|
||||
disableMismatchWarning,
|
||||
navigationAction,
|
||||
openLink,
|
||||
],
|
||||
)
|
||||
const hrefAttrs = useMemo(() => {
|
||||
const isExternal = isExternalUrl(href)
|
||||
if (isExternal) {
|
||||
@@ -413,22 +400,15 @@ function onPressInner(
|
||||
} else {
|
||||
closeModal() // close any active modals
|
||||
|
||||
const [routeName, params] = router.matchPath(href)
|
||||
if (navigationAction === 'push') {
|
||||
// @ts-ignore we're not able to type check on this one -prf
|
||||
navigation.dispatch(StackActions.push(routeName, params))
|
||||
navigation.dispatch(StackActions.push(...router.matchPath(href)))
|
||||
} else if (navigationAction === 'replace') {
|
||||
// @ts-ignore we're not able to type check on this one -prf
|
||||
navigation.dispatch(StackActions.replace(routeName, params))
|
||||
navigation.dispatch(StackActions.replace(...router.matchPath(href)))
|
||||
} else if (navigationAction === 'navigate') {
|
||||
const state = navigation.getState()
|
||||
const tabState = getTabState(state, routeName)
|
||||
if (tabState === TabState.InsideAtRoot) {
|
||||
emitSoftReset()
|
||||
} else {
|
||||
// @ts-ignore we're not able to type check on this one -prf
|
||||
navigation.navigate(routeName, params)
|
||||
}
|
||||
// @ts-ignore we're not able to type check on this one -prf
|
||||
navigation.navigate(...router.matchPath(href))
|
||||
} else {
|
||||
throw Error('Unsupported navigator action.')
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||
import {addStyle} from '#/lib/styles'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useLightbox} from '#/state/lightbox'
|
||||
import {useTheme} from '#/alf'
|
||||
import {FlatList_INTERNAL} from './Views'
|
||||
|
||||
@@ -53,7 +52,6 @@ function ListImpl<ItemT>(
|
||||
const isScrolledDown = useSharedValue(false)
|
||||
const t = useTheme()
|
||||
const dedupe = useDedupe(400)
|
||||
const {activeLightbox} = useLightbox()
|
||||
|
||||
function handleScrolledDownChange(didScrollDown: boolean) {
|
||||
onScrolledDownChange?.(didScrollDown)
|
||||
@@ -79,8 +77,8 @@ function ListImpl<ItemT>(
|
||||
onScrollFromContext?.(e, ctx)
|
||||
|
||||
const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
|
||||
if (isScrolledDown.get() !== didScrollDown) {
|
||||
isScrolledDown.set(didScrollDown)
|
||||
if (isScrolledDown.value !== didScrollDown) {
|
||||
isScrolledDown.value = didScrollDown
|
||||
if (onScrolledDownChange != null) {
|
||||
runOnJS(handleScrolledDownChange)(didScrollDown)
|
||||
}
|
||||
@@ -145,7 +143,6 @@ function ListImpl<ItemT>(
|
||||
contentOffset={contentOffset}
|
||||
refreshControl={refreshControl}
|
||||
onScroll={scrollHandler}
|
||||
scrollsToTop={!activeLightbox}
|
||||
scrollEventThrottle={1}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
viewabilityConfig={viewabilityConfig}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
(v: boolean) => {
|
||||
'worklet'
|
||||
cancelAnimation(headerMode)
|
||||
headerMode.set(v ? V1.get() : V0.get())
|
||||
headerMode.value = v ? V1.value : V0.value
|
||||
},
|
||||
[headerMode],
|
||||
)
|
||||
@@ -52,9 +52,9 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
useEffect(() => {
|
||||
if (isWeb) {
|
||||
return listenToForcedWindowScroll(() => {
|
||||
startDragOffset.set(null)
|
||||
startMode.set(null)
|
||||
didJustRestoreScroll.set(true)
|
||||
startDragOffset.value = null
|
||||
startMode.value = null
|
||||
didJustRestoreScroll.value = true
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -63,14 +63,13 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
if (isNative) {
|
||||
const startDragOffsetValue = startDragOffset.get()
|
||||
if (startDragOffsetValue === null) {
|
||||
if (startDragOffset.value === null) {
|
||||
return
|
||||
}
|
||||
const didScrollDown = e.contentOffset.y > startDragOffsetValue
|
||||
startDragOffset.set(null)
|
||||
startMode.set(null)
|
||||
if (e.contentOffset.y < headerHeight.get()) {
|
||||
const didScrollDown = e.contentOffset.y > startDragOffset.value
|
||||
startDragOffset.value = null
|
||||
startMode.value = null
|
||||
if (e.contentOffset.y < headerHeight.value) {
|
||||
// If we're close to the top, show the shell.
|
||||
setMode(false)
|
||||
} else if (didScrollDown) {
|
||||
@@ -78,7 +77,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
setMode(true)
|
||||
} else {
|
||||
// Snap to whichever state is the closest.
|
||||
setMode(Math.round(headerMode.get()) === 1)
|
||||
setMode(Math.round(headerMode.value) === 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -89,8 +88,8 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
if (isNative) {
|
||||
startDragOffset.set(e.contentOffset.y)
|
||||
startMode.set(headerMode.get())
|
||||
startDragOffset.value = e.contentOffset.y
|
||||
startMode.value = headerMode.value
|
||||
}
|
||||
},
|
||||
[headerMode, startDragOffset, startMode],
|
||||
@@ -124,12 +123,10 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
if (isNative) {
|
||||
const startDragOffsetValue = startDragOffset.get()
|
||||
const startModeValue = startMode.get()
|
||||
if (startDragOffsetValue === null || startModeValue === null) {
|
||||
if (startDragOffset.value === null || startMode.value === null) {
|
||||
if (
|
||||
headerMode.get() !== 0 &&
|
||||
e.contentOffset.y < headerHeight.get()
|
||||
headerMode.value !== 0 &&
|
||||
e.contentOffset.y < headerHeight.value
|
||||
) {
|
||||
// If we're close enough to the top, always show the shell.
|
||||
// Even if we're not dragging.
|
||||
@@ -140,29 +137,29 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
|
||||
// The "mode" value is always between 0 and 1.
|
||||
// Figure out how much to move it based on the current dragged distance.
|
||||
const dy = e.contentOffset.y - startDragOffsetValue
|
||||
const dy = e.contentOffset.y - startDragOffset.value
|
||||
const dProgress = interpolate(
|
||||
dy,
|
||||
[-headerHeight.get(), headerHeight.get()],
|
||||
[-headerHeight.value, headerHeight.value],
|
||||
[-1, 1],
|
||||
)
|
||||
const newValue = clamp(startModeValue + dProgress, 0, 1)
|
||||
if (newValue !== headerMode.get()) {
|
||||
const newValue = clamp(startMode.value + dProgress, 0, 1)
|
||||
if (newValue !== headerMode.value) {
|
||||
// Manually adjust the value. This won't be (and shouldn't be) animated.
|
||||
// Cancel any any existing animation
|
||||
cancelAnimation(headerMode)
|
||||
headerMode.set(newValue)
|
||||
headerMode.value = newValue
|
||||
}
|
||||
} else {
|
||||
if (didJustRestoreScroll.get()) {
|
||||
didJustRestoreScroll.set(false)
|
||||
if (didJustRestoreScroll.value) {
|
||||
didJustRestoreScroll.value = false
|
||||
// Don't hide/show navbar based on scroll restoratoin.
|
||||
return
|
||||
}
|
||||
// On the web, we don't try to follow the drag because we don't know when it ends.
|
||||
// Instead, show/hide immediately based on whether we're scrolling up or down.
|
||||
const dy = e.contentOffset.y - (startDragOffset.get() ?? 0)
|
||||
startDragOffset.set(e.contentOffset.y)
|
||||
const dy = e.contentOffset.y - (startDragOffset.value ?? 0)
|
||||
startDragOffset.value = e.contentOffset.y
|
||||
|
||||
if (dy < 0 || e.contentOffset.y < WEB_HIDE_SHELL_THRESHOLD) {
|
||||
setMode(false)
|
||||
|
||||
@@ -96,8 +96,6 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
paddingTop: Math.abs(contentOffset.y),
|
||||
})
|
||||
}
|
||||
// @ts-ignore web only -prf
|
||||
let dataSet = props.dataSet
|
||||
if (desktopFixedHeight) {
|
||||
if (typeof desktopFixedHeight === 'number') {
|
||||
// @ts-ignore Web only -prf
|
||||
@@ -116,10 +114,10 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
// around this, we set data-stable-gutters which can then be
|
||||
// styled in our external CSS.
|
||||
// -prf
|
||||
dataSet = {
|
||||
...dataSet,
|
||||
stableGutters: '1',
|
||||
}
|
||||
// @ts-ignore web only -prf
|
||||
props.dataSet = props.dataSet || {}
|
||||
// @ts-ignore web only -prf
|
||||
props.dataSet.stableGutters = '1'
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -133,8 +131,6 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
style={style}
|
||||
contentOffset={contentOffset}
|
||||
{...props}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={dataSet}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -102,77 +102,12 @@ let PostDropdownBtn = ({
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
}): React.ReactNode => {
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const theme = useTheme()
|
||||
const alf = useAlf()
|
||||
const {_} = useLingui()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
return (
|
||||
<EventStopper onKeyDown={false}>
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Open post options menu`)}>
|
||||
{({props, state}) => {
|
||||
return (
|
||||
<Pressable
|
||||
{...props}
|
||||
hitSlop={hitSlop}
|
||||
testID={testID}
|
||||
style={[
|
||||
style,
|
||||
a.rounded_full,
|
||||
(state.hovered || state.pressed) && [
|
||||
alf.atoms.bg_contrast_25,
|
||||
],
|
||||
]}>
|
||||
<DotsHorizontal
|
||||
fill={defaultCtrlColor}
|
||||
style={{pointerEvents: 'none'}}
|
||||
size={size}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}}
|
||||
</Menu.Trigger>
|
||||
<Menu.Outer>
|
||||
<PostDropdownMenuItems
|
||||
testID={testID}
|
||||
post={post}
|
||||
postFeedContext={postFeedContext}
|
||||
record={record}
|
||||
richText={richText}
|
||||
timestamp={timestamp}
|
||||
threadgateRecord={threadgateRecord}
|
||||
/>
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
</EventStopper>
|
||||
)
|
||||
}
|
||||
|
||||
PostDropdownBtn = memo(PostDropdownBtn)
|
||||
export {PostDropdownBtn}
|
||||
|
||||
let PostDropdownMenuItems = ({
|
||||
post,
|
||||
postFeedContext,
|
||||
record,
|
||||
richText,
|
||||
timestamp,
|
||||
threadgateRecord,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
postFeedContext: string | undefined
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
}): React.ReactNode => {
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {_} = useLingui()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
|
||||
const {mutateAsync: pinPostMutate, isPending: isPinPending} =
|
||||
@@ -306,7 +241,7 @@ let PostDropdownMenuItems = ({
|
||||
}, [_, richText])
|
||||
|
||||
const onPressTranslate = React.useCallback(async () => {
|
||||
await openLink(translatorUrl, true)
|
||||
await openLink(translatorUrl)
|
||||
}, [openLink, translatorUrl])
|
||||
|
||||
const onHidePost = React.useCallback(() => {
|
||||
@@ -425,276 +360,308 @@ let PostDropdownMenuItems = ({
|
||||
}, [isPinned, pinPostMutate, postCid, postUri])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAuthor && (
|
||||
<>
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="pinPostBtn"
|
||||
label={
|
||||
isPinned
|
||||
? _(msg`Unpin from profile`)
|
||||
: _(msg`Pin to your profile`)
|
||||
}
|
||||
disabled={isPinPending}
|
||||
onPress={onPressPin}>
|
||||
<Menu.ItemText>
|
||||
{isPinned
|
||||
? _(msg`Unpin from profile`)
|
||||
: _(msg`Pin to your profile`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isPinPending ? Loader : PinIcon}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
<EventStopper onKeyDown={false}>
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Open post options menu`)}>
|
||||
{({props, state}) => {
|
||||
return (
|
||||
<Pressable
|
||||
{...props}
|
||||
hitSlop={hitSlop}
|
||||
testID={testID}
|
||||
style={[
|
||||
style,
|
||||
a.rounded_full,
|
||||
(state.hovered || state.pressed) && [
|
||||
alf.atoms.bg_contrast_25,
|
||||
],
|
||||
]}>
|
||||
<DotsHorizontal
|
||||
fill={defaultCtrlColor}
|
||||
style={{pointerEvents: 'none'}}
|
||||
size={size}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}}
|
||||
</Menu.Trigger>
|
||||
|
||||
<Menu.Group>
|
||||
{(!hideInPWI || hasSession) && (
|
||||
<>
|
||||
<Menu.Item
|
||||
testID="postDropdownTranslateBtn"
|
||||
label={_(msg`Translate`)}
|
||||
onPress={onPressTranslate}>
|
||||
<Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Translate} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownCopyTextBtn"
|
||||
label={_(msg`Copy post text`)}
|
||||
onPress={onCopyPostText}>
|
||||
<Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<Menu.Item
|
||||
testID="postDropdownSendViaDMBtn"
|
||||
label={_(msg`Send via direct message`)}
|
||||
onPress={() => sendViaChatControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Send via direct message</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Send} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownShareBtn"
|
||||
label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
|
||||
onPress={() => {
|
||||
if (showLoggedOutWarning) {
|
||||
loggedOutWarningPromptControl.open()
|
||||
} else {
|
||||
onSharePost()
|
||||
}
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
{isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Share} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
{canEmbed && (
|
||||
<Menu.Item
|
||||
testID="postDropdownEmbedBtn"
|
||||
label={_(msg`Embed post`)}
|
||||
onPress={() => embedPostControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={CodeBrackets} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
|
||||
{hasSession && feedFeedback.enabled && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownShowMoreBtn"
|
||||
label={_(msg`Show more like this`)}
|
||||
onPress={onPressShowMore}>
|
||||
<Menu.ItemText>{_(msg`Show more like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSmile} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownShowLessBtn"
|
||||
label={_(msg`Show less like this`)}
|
||||
onPress={onPressShowLess}>
|
||||
<Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSad} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteThreadBtn"
|
||||
label={
|
||||
isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
|
||||
}
|
||||
onPress={onToggleThreadMute}>
|
||||
<Menu.ItemText>
|
||||
{isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isThreadMuted ? Unmute : Mute}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteWordsBtn"
|
||||
label={_(msg`Mute words & tags`)}
|
||||
onPress={() => mutedWordsDialogControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Filter} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession &&
|
||||
(canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
{canHidePostForMe && (
|
||||
<Menu.Outer>
|
||||
{isAuthor && (
|
||||
<>
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownHideBtn"
|
||||
testID="pinPostBtn"
|
||||
label={
|
||||
isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)
|
||||
isPinned
|
||||
? _(msg`Unpin from profile`)
|
||||
: _(msg`Pin to your profile`)
|
||||
}
|
||||
onPress={() => hidePromptControl.open()}>
|
||||
disabled={isPinPending}
|
||||
onPress={onPressPin}>
|
||||
<Menu.ItemText>
|
||||
{isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EyeSlash} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canHideReplyForEveryone && (
|
||||
<Menu.Item
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
isReplyHiddenByThreadgate
|
||||
? _(msg`Show reply for everyone`)
|
||||
: _(msg`Hide reply for everyone`)
|
||||
}
|
||||
onPress={
|
||||
isReplyHiddenByThreadgate
|
||||
? onToggleReplyVisibility
|
||||
: () => hideReplyConfirmControl.open()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{isReplyHiddenByThreadgate
|
||||
? _(msg`Show reply for everyone`)
|
||||
: _(msg`Hide reply for everyone`)}
|
||||
{isPinned
|
||||
? _(msg`Unpin from profile`)
|
||||
: _(msg`Pin to your profile`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isReplyHiddenByThreadgate ? Eye : EyeSlash}
|
||||
icon={isPinPending ? Loader : PinIcon}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{canDetachQuote && (
|
||||
<Menu.Item
|
||||
disabled={isDetachPending}
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
quoteEmbed.isDetached
|
||||
? _(msg`Re-attach quote`)
|
||||
: _(msg`Detach quote`)
|
||||
}
|
||||
onPress={
|
||||
quoteEmbed.isDetached
|
||||
? onToggleQuotePostAttachment
|
||||
: () => quotePostDetachConfirmControl.open()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{quoteEmbed.isDetached
|
||||
? _(msg`Re-attach quote`)
|
||||
: _(msg`Detach quote`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={
|
||||
isDetachPending
|
||||
? Loader
|
||||
: quoteEmbed.isDetached
|
||||
? Eye
|
||||
: EyeSlash
|
||||
}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
{!isAuthor && (
|
||||
<Menu.Item
|
||||
testID="postDropdownReportBtn"
|
||||
label={_(msg`Report post`)}
|
||||
onPress={() => reportDialogControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Warning} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{isAuthor && (
|
||||
{(!hideInPWI || hasSession) && (
|
||||
<>
|
||||
<Menu.Item
|
||||
testID="postDropdownEditPostInteractions"
|
||||
label={_(msg`Edit interaction settings`)}
|
||||
onPress={() => postInteractionSettingsDialogControl.open()}
|
||||
{...(isAuthor
|
||||
? Platform.select({
|
||||
web: {
|
||||
onHoverIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
native: {
|
||||
onPressIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
})
|
||||
: {})}>
|
||||
<Menu.ItemText>
|
||||
{_(msg`Edit interaction settings`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Gear} position="right" />
|
||||
testID="postDropdownTranslateBtn"
|
||||
label={_(msg`Translate`)}
|
||||
onPress={onPressTranslate}>
|
||||
<Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Translate} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownDeleteBtn"
|
||||
label={_(msg`Delete post`)}
|
||||
onPress={() => deletePromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
testID="postDropdownCopyTextBtn"
|
||||
label={_(msg`Copy post text`)}
|
||||
onPress={onCopyPostText}>
|
||||
<Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<Menu.Item
|
||||
testID="postDropdownSendViaDMBtn"
|
||||
label={_(msg`Send via direct message`)}
|
||||
onPress={() => sendViaChatControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Send via direct message</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Send} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownShareBtn"
|
||||
label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
|
||||
onPress={() => {
|
||||
if (showLoggedOutWarning) {
|
||||
loggedOutWarningPromptControl.open()
|
||||
} else {
|
||||
onSharePost()
|
||||
}
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
{isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Share} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
{canEmbed && (
|
||||
<Menu.Item
|
||||
testID="postDropdownEmbedBtn"
|
||||
label={_(msg`Embed post`)}
|
||||
onPress={() => embedPostControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={CodeBrackets} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && feedFeedback.enabled && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownShowMoreBtn"
|
||||
label={_(msg`Show more like this`)}
|
||||
onPress={onPressShowMore}>
|
||||
<Menu.ItemText>{_(msg`Show more like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSmile} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownShowLessBtn"
|
||||
label={_(msg`Show less like this`)}
|
||||
onPress={onPressShowLess}>
|
||||
<Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSad} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteThreadBtn"
|
||||
label={
|
||||
isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
|
||||
}
|
||||
onPress={onToggleThreadMute}>
|
||||
<Menu.ItemText>
|
||||
{isThreadMuted
|
||||
? _(msg`Unmute thread`)
|
||||
: _(msg`Mute thread`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isThreadMuted ? Unmute : Mute}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteWordsBtn"
|
||||
label={_(msg`Mute words & tags`)}
|
||||
onPress={() => mutedWordsDialogControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Filter} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession &&
|
||||
(canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
{canHidePostForMe && (
|
||||
<Menu.Item
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)
|
||||
}
|
||||
onPress={() => hidePromptControl.open()}>
|
||||
<Menu.ItemText>
|
||||
{isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EyeSlash} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canHideReplyForEveryone && (
|
||||
<Menu.Item
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
isReplyHiddenByThreadgate
|
||||
? _(msg`Show reply for everyone`)
|
||||
: _(msg`Hide reply for everyone`)
|
||||
}
|
||||
onPress={
|
||||
isReplyHiddenByThreadgate
|
||||
? onToggleReplyVisibility
|
||||
: () => hideReplyConfirmControl.open()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{isReplyHiddenByThreadgate
|
||||
? _(msg`Show reply for everyone`)
|
||||
: _(msg`Hide reply for everyone`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isReplyHiddenByThreadgate ? Eye : EyeSlash}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{canDetachQuote && (
|
||||
<Menu.Item
|
||||
disabled={isDetachPending}
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
quoteEmbed.isDetached
|
||||
? _(msg`Re-attach quote`)
|
||||
: _(msg`Detach quote`)
|
||||
}
|
||||
onPress={
|
||||
quoteEmbed.isDetached
|
||||
? onToggleQuotePostAttachment
|
||||
: () => quotePostDetachConfirmControl.open()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{quoteEmbed.isDetached
|
||||
? _(msg`Re-attach quote`)
|
||||
: _(msg`Detach quote`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={
|
||||
isDetachPending
|
||||
? Loader
|
||||
: quoteEmbed.isDetached
|
||||
? Eye
|
||||
: EyeSlash
|
||||
}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
{!isAuthor && (
|
||||
<Menu.Item
|
||||
testID="postDropdownReportBtn"
|
||||
label={_(msg`Report post`)}
|
||||
onPress={() => reportDialogControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Warning} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{isAuthor && (
|
||||
<>
|
||||
<Menu.Item
|
||||
testID="postDropdownEditPostInteractions"
|
||||
label={_(msg`Edit interaction settings`)}
|
||||
onPress={() =>
|
||||
postInteractionSettingsDialogControl.open()
|
||||
}
|
||||
{...(isAuthor
|
||||
? Platform.select({
|
||||
web: {
|
||||
onHoverIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
native: {
|
||||
onPressIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
})
|
||||
: {})}>
|
||||
<Menu.ItemText>
|
||||
{_(msg`Edit interaction settings`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Gear} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="postDropdownDeleteBtn"
|
||||
label={_(msg`Delete post`)}
|
||||
onPress={() => deletePromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
|
||||
<Prompt.Basic
|
||||
control={deletePromptControl}
|
||||
@@ -778,7 +745,9 @@ let PostDropdownMenuItems = ({
|
||||
onConfirm={onToggleReplyVisibility}
|
||||
confirmButtonCta={_(msg`Yes, hide`)}
|
||||
/>
|
||||
</>
|
||||
</EventStopper>
|
||||
)
|
||||
}
|
||||
PostDropdownMenuItems = memo(PostDropdownMenuItems)
|
||||
|
||||
PostDropdownBtn = memo(PostDropdownBtn)
|
||||
export {PostDropdownBtn}
|
||||
|
||||
+31
-38
@@ -122,8 +122,9 @@ let DrawerProfileCard = ({
|
||||
DrawerProfileCard = React.memo(DrawerProfileCard)
|
||||
export {DrawerProfileCard}
|
||||
|
||||
let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
|
||||
let DrawerContent = ({}: {}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const insets = useSafeAreaInsets()
|
||||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
@@ -136,6 +137,7 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
|
||||
isAtMessages,
|
||||
} = useNavigationTabState()
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const kawaii = useKawaiiMode()
|
||||
|
||||
// events
|
||||
// =
|
||||
@@ -275,7 +277,34 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
|
||||
|
||||
<View style={[a.px_xl]}>
|
||||
<Divider style={[a.mb_xl, a.mt_sm]} />
|
||||
<ExtraLinks />
|
||||
|
||||
<View style={[a.flex_col, a.gap_md, a.flex_wrap]}>
|
||||
<InlineLinkText
|
||||
style={[a.text_md]}
|
||||
label={_(msg`Terms of Service`)}
|
||||
to="https://bsky.social/about/support/tos">
|
||||
<Trans>Terms of Service</Trans>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText
|
||||
style={[a.text_md]}
|
||||
to="https://bsky.social/about/support/privacy-policy"
|
||||
label={_(msg`Privacy Policy`)}>
|
||||
<Trans>Privacy Policy</Trans>
|
||||
</InlineLinkText>
|
||||
{kawaii && (
|
||||
<Text style={t.atoms.text_contrast_medium}>
|
||||
<Trans>
|
||||
Logo by{' '}
|
||||
<InlineLinkText
|
||||
style={[a.text_md]}
|
||||
to="/profile/sawaratsuki.bsky.social"
|
||||
label="@sawaratsuki.bsky.social">
|
||||
@sawaratsuki.bsky.social
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -604,39 +633,3 @@ function MenuItem({icon, label, count, bold, onPress}: MenuItemProps) {
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function ExtraLinks() {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const kawaii = useKawaiiMode()
|
||||
|
||||
return (
|
||||
<View style={[a.flex_col, a.gap_md, a.flex_wrap]}>
|
||||
<InlineLinkText
|
||||
style={[a.text_md]}
|
||||
label={_(msg`Terms of Service`)}
|
||||
to="https://bsky.social/about/support/tos">
|
||||
<Trans>Terms of Service</Trans>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText
|
||||
style={[a.text_md]}
|
||||
to="https://bsky.social/about/support/privacy-policy"
|
||||
label={_(msg`Privacy Policy`)}>
|
||||
<Trans>Privacy Policy</Trans>
|
||||
</InlineLinkText>
|
||||
{kawaii && (
|
||||
<Text style={t.atoms.text_contrast_medium}>
|
||||
<Trans>
|
||||
Logo by{' '}
|
||||
<InlineLinkText
|
||||
style={[a.text_md]}
|
||||
to="/profile/sawaratsuki.bsky.social"
|
||||
label="@sawaratsuki.bsky.social">
|
||||
@sawaratsuki.bsky.social
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
||||
footerMinimalShellTransform,
|
||||
]}
|
||||
onLayout={e => {
|
||||
footerHeight.set(e.nativeEvent.layout.height)
|
||||
footerHeight.value = e.nativeEvent.layout.height
|
||||
}}>
|
||||
{hasSession ? (
|
||||
<>
|
||||
|
||||
@@ -105,7 +105,6 @@ function ShellInner() {
|
||||
onOpen={onOpenDrawer}
|
||||
onClose={onCloseDrawer}
|
||||
swipeEdgeWidth={winDim.width / 2}
|
||||
drawerType={isIOS ? 'slide' : 'front'}
|
||||
swipeEnabled={!canGoBack && hasSession && !isDrawerSwipeDisabled}
|
||||
overlayStyle={{
|
||||
backgroundColor: select(t.name, {
|
||||
|
||||
Reference in New Issue
Block a user