Compare commits

..

43 Commits

Author SHA1 Message Date
Dan Abramov 6e4646e1a9 Fix React Rules violations, enable strict lint 2024-11-17 20:40:51 +00:00
Dan Abramov c2ee0ee910 Make React rules violation an error 2024-11-17 19:03:42 +00:00
dan 2f74a8dbe9 Upgrade React Compiler (take two) (#6460)
* Revert "Revert "React compiler beta and reenable rule (#5898)" (#6071)"

This reverts commit 7111e38cd4.

* Update the compiler

* Don't inline compiler runtime import
2024-11-17 18:50:28 +00:00
dan dd9a2f60dd Fix build (#6459) 2024-11-17 17:26:45 +00:00
dan 474c4eff29 Use compiler-safe Reanimated get/set APIs (#6391)
* Convert lightbox to get/set

* Work around software-mansion/react-native-reanimated#6613

* Use get/set in more places

* Port MainScrollProvider to get/set

* Port more to get/set

* Port composer to get/set

* Remove unnecessary thread hops in composer

* Port more things to get/set

* Convert more to get/set, remove redundant runOnJS

* Convert remaining cases to get/set
2024-11-17 15:06:28 +00:00
Trivikram Kamat d575a2fdaa Remove unused 'react-scripts' devDependency (#6441) 2024-11-17 14:57:01 +00:00
Samuel Newman 8d1c93efc8 fix video thumb cleanup (#6431) 2024-11-17 14:56:15 +00:00
dan e9fe8d90ef Render dropdown menu items lazily (#6437) 2024-11-17 03:15:03 +00:00
dan ddf2a64a72 [Web] Clicking root link twice refreshes the screen (#6434)
* [Web] Clicking root link twice refreshes the screen

* Scope it to navigation action

In practice this means -- just for the bottom mobile web tab bar.
2024-11-17 00:37:34 +00:00
Paul Frazee 3c30bb1d35 Release 1.94 (#6430)
* Fix errors in hindi .po

* Run intl extract

* Update join count
2024-11-16 15:12:06 -08:00
dan 965bcc44dc Sort pins last in thread view (#6426) 2024-11-16 22:02:33 +00:00
dan edaf230612 Remove typography NUX and consistently call upsertProfile (#6424)
* Make profile upsert unconditional

* Remove Typography NUX

* Early exit NUXes on unknown creation date

* Clarify via comment
2024-11-16 21:39:47 +00:00
Hailey 9bbea3f33a Email verification tweaks (date) (#6416) 2024-11-16 13:34:30 -08:00
Luan 43d6c15e88 Update PT-BR messages.po (#6284)
* Update PT-BR translation

* Update PT-BR translation

* Update messages.po

* Update PT-BR translation for consistent usage of "postagem"

* Update messages.po

* Update src/locale/locales/pt-BR/messages.po

Co-authored-by: Gildásio Filho <gildasiogx@gmail.com>

* Update src/locale/locales/pt-BR/messages.po

* Update src/locale/locales/pt-BR/messages.po

* Update src/locale/locales/pt-BR/messages.po

* Update src/locale/locales/pt-BR/messages.po

* Apply suggestions from code review

Co-authored-by: Gildásio Filho <gildasiogx@gmail.com>

---------

Co-authored-by: Gildásio Filho <gildasiogx@gmail.com>
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2024-11-16 12:44:11 -08:00
CanGiante 4e1ec0db86 Fix the translation of the adjective "open" (#6206) 2024-11-16 12:27:06 -08:00
Elena Torró d8e2a90dd7 Fix minor localization issues in Spanish: (#6094)
* Fix minor localization issues in Spanish:

- Use 'pack de inicio' instead of 'paquete de inicio'
- Use 'Inténtalo' instead of 'Intenta'
- Fix 'Por favor'
- Do not use 'una' as article for 'posts'
- Use 'Eliminado' instead of 'Removido'

* Update src/locale/locales/es/messages.po

Co-authored-by: oledfish <88390729+oledfish@users.noreply.github.com>

* Use 'paquete de inicio' and fix typos

---------

Co-authored-by: oledfish <88390729+oledfish@users.noreply.github.com>
2024-11-16 12:15:42 -08:00
Kevin Scannell 1f9c94a3cc Update Irish localization to 100% (#6387) 2024-11-16 12:13:27 -08:00
Stanislas Signoud 8e40b1147a Update French translations (November 9th, 2024) (#6190)
* Remove legacy strings in FR localisation file

* Update French translations

* Fix typos and inconsistent French translations

* Get rid of a few 'Veuillez' in French locale to appear less formal
2024-11-16 12:08:59 -08:00
Marco Maroni 0fd6eeb025 Update italian translation (#6188)
* Updated and added missing italian translation

* Resolved conflicts

* Fix conflict

* Fix some translation

* Update messages.po

Some other fixes
2024-11-16 12:08:22 -08:00
FalKoopa 330ed124d6 Big update of the Hindi translation (#6077)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Resolve conflicts

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po based on feedback

* Update messages.po

* Update messages.po
2024-11-16 12:08:02 -08:00
Ivan Beà 01469b092f Update catalan messages.po (#6030)
* Update catalan messages.po

back to 100%
Please have a look at it @jordimas @darccio @surfdude29 @rortan134

* Update catalan messages.po

aplying @jordimas corrections

* Update messages.po
2024-11-16 12:07:17 -08:00
Samuel Newman c81e149749 use IAB for translate (#6421) 2024-11-16 20:06:24 +00:00
Samuel Newman ebdbf55d14 manually implement drawertype logic (#6419) 2024-11-16 20:06:11 +00:00
Frudrax Cheng 6aa14f3203 Update Chinese & Cantonese localization (#6020)
* New translations messages.po (Chinese Simplified)

* New translations messages.po (Chinese Traditional)

* New translations messages.po (Chinese Traditional, Hong Kong)

* Update translates from crowdin

* Run intl:extract

* Update translates from crowdin

* Update translates from crowdin

* zh-CN: Fix typo

* zh-HK: Update translates

* Update translates by notlin4

* Update translates from crowdin

* Update translates from crowdin

* zh-CN: Clarify description text

* Update translates from crowdin

* zh-HK: Update translates

* zh-CN: Re-translate 'Feed'

* zh-HK: Update Translates

* Update translates from crowdin

* CN & TW: Consistency fixes

* Update translates from crowdin

* Update translates from crowdin

* Unify the translation of 'Yes'

* Update translates from crowdin

* Update translates from crowdin

* zh-CN: Optimize media tag translates

* CN & HK: Update translates from crowdin

* Improved 'starter pack' translations

* CN & HK: Improved onboard translations

* zh-CN: Improved translation

* Update translates from crowdin

* zh-HK: Improved translation

* Remove extra spaces before the time

* Update Translates from crowdin

* Update translates from crowdin

* zh-CN: Improved translation

* CN: Fix 'mute for everyone' translates

* Retranslate 'self-labels'

* Retranslate 'self-labels' again

* CN & HK: Improved translation

* CN & HK: Improved translation#2

* CN: Improved translation

* Rollback 'report' translation

* Hot fix

* CN: Clarify label annotations

* TW: Improved translation

* Update translates from crowdin

* Translated email verification dialog & Fix HK typo
2024-11-16 12:05:30 -08:00
Minseo Lee 0f1abfa9d4 Update Korean localization (#6014)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po
2024-11-16 12:04:59 -08:00
Takayuki KUSANO 71d5dc0885 Update Japanese translation (#6013)
* Update translations

* Remove obsoleted messages

* Fix the msg pointed out by Hima-Zinn

* Update translation

* Update translation

* Applied suggested fixes by Hima-Zinn

* Translate 1 message and apply middlingphys' suggesstion

* Translate unread related msgs, etc.

* Update translation

* Translate 2 new msgs, remove obsolete msgs

* Translate 1 message

* translate backdated info

* Update translation
2024-11-16 12:04:36 -08:00
Hailey c0fb5245f1 Tweak email verification dialog (#6397) 2024-11-15 17:32:28 -08:00
Samuel Newman a9cacbf265 Better error message (+ translations!) (#6349)
* clean network errors for new common eerors

* translate errors
2024-11-15 22:16:57 +00:00
Hailey 3bd1437133 [Statsig] Remove client downsampling (#6153) 2024-11-15 11:40:43 -08:00
Eric Bailey 400c432283 Reduce calls to getLogs (#6381) 2024-11-15 11:43:21 -06:00
Eric Bailey db39f3e98a Reduce listConvos requests (#6378)
* Reduce page size for request

* Remove refetch interval entirely

* Add comment

* Optimistically mark as read

* Drop default active poll interval to 60s from 5min

* Only optimistically update unread count if success
2024-11-15 11:34:31 -06:00
dan 18aaa19b97 [Lightbox] Make the animation faster (#6382)
* [Lightbox] Make the animation faster

* Make it faster on Android too
2024-11-15 17:32:25 +00:00
Marc Rousavy 05312cce33 feat: Tweak shared element animation to make it much smoother (#6336) 2024-11-14 19:37:41 +00:00
Bartosz Kaszubowski 7d34f6bb5c NewChatDialog: remove extra padding on web (#6333) 2024-11-14 17:56:33 +00:00
Eric Bailey 5f7caf3010 Make ToggleButton full width everywhere (#6302)
* Fix 'Show badge' button label being cut off (#6215)

* Make em full width

---------

Co-authored-by: Khuddite <62555977+khuddite@users.noreply.github.com>
2024-11-13 21:55:19 +00:00
Paul Frazee 9640f08b8d Modify self labels for clarity 2024-11-13 21:54:24 +00:00
Paul Frazee ea0a1c3200 Test fixes 2024-11-13 21:54:24 +00:00
dan 4ca5267b98 [Lightbox] Small tweaks (#6301)
* Tweak lightbox springs

* Speed up dismiss speed

* Don't scroll to top while active lightbox

* Make Android a bit faster
2024-11-13 21:13:07 +00:00
dan 3bab7f7540 Patch RCTScrollView to fix centerContent (#6298) 2024-11-13 16:34:24 +00:00
Samuel Newman 017b7647b1 Add subtle web hover to treeview replies (#6142)
* add subtle web hover to treeview replies

* don't highlight indentation

* adjust position

* clean up some styles
2024-11-13 14:25:56 +00:00
Khuddite aa55798e55 Upgrade react-native-drawer-layout version to v4.0.1 (#6274) 2024-11-12 23:47:28 +00:00
Samuel Newman 6471e809aa Add backdated post indicator (#6156)
* add backdate indicator

* pill style

* add indexedAt

* update indicator - new copy, date in pill

* complete alf migration

* accidentally committed the missing indexedAt *again*!

* copy tweak
2024-11-12 21:02:41 +00:00
Samuel Newman f2916ce528 Slightly reduce rerenders in drawer (#6270)
* slightly reduce renders in drawer

* fix missing hook
2024-11-12 20:44:13 +00:00
100 changed files with 37567 additions and 31275 deletions
+3 -2
View File
@@ -1,3 +1,5 @@
const reactCompilerConfig = require('./react-compiler.config.js')
module.exports = {
root: true,
extends: [
@@ -79,8 +81,7 @@ module.exports = {
},
],
'simple-import-sort/exports': 'error',
// TODO: Reenable when we figure out why it gets stuck on CI.
// 'react-compiler/react-compiler': 'error',
'react-compiler/react-compiler': ['error', reactCompilerConfig],
'no-restricted-imports': [
'error',
{
+2 -2
View File
@@ -17,8 +17,8 @@ appId: xyz.blueskyweb.app
- inputText: "Post with an image"
- tapOn:
id: "openGalleryBtn"
- tapOn:
id: "labelsBtn"
- tapOn: "Content warnings"
- tapOn: "Content warnings"
- tapOn: "Porn"
- tapOn:
label: "Tap on confirm"
-1
View File
@@ -29,7 +29,6 @@ appId: xyz.blueskyweb.app
- pressKey: Enter
- tapOn:
id: "nextBtn"
- tapOn: "Not now"
- tapOn:
id: "handleInput"
- inputText: "e2e-test"
+4
View File
@@ -23,6 +23,7 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- inputText: "They good"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- tapOn: "About"
@@ -45,6 +46,7 @@ appId: xyz.blueskyweb.app
- eraseText
- inputText: "They bad"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- assertVisible: Bad Ppl
@@ -60,6 +62,7 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- eraseText
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- assertNotVisible:
@@ -87,6 +90,7 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- inputText: "They good"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- tapOn: "About"
-1
View File
@@ -23,5 +23,4 @@ appId: xyz.blueskyweb.app
id: "loginPasswordInput"
- inputText: "hunter2"
- pressKey: Enter
- tapOn: "Not now"
- assertVisible: "Following"
+1
View File
@@ -22,6 +22,7 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- inputText: "Shhh"
- tapOn: "Save"
- tapOn: "Save"
# view modlist
- assertVisible: "Muted Users"
+1 -1
View File
@@ -13,7 +13,7 @@ appId: xyz.blueskyweb.app
- tapOn: "Select an avatar"
- waitForAnimationToEnd
- tapOn:
point: "16%,22%"
point: "50%,22%"
- waitForAnimationToEnd
- tapOn: "Choose"
- waitForAnimationToEnd
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 589 B

+3 -6
View File
@@ -1,3 +1,5 @@
const reactCompilerConfig = require('./react-compiler.config.js')
module.exports = function (api) {
api.cache(true)
const isTestEnv = process.env.NODE_ENV === 'test'
@@ -17,12 +19,7 @@ module.exports = function (api) {
],
plugins: [
'macros',
[
'babel-plugin-react-compiler',
{
runtimeModule: 'react-compiler-runtime',
},
],
['babel-plugin-react-compiler', reactCompilerConfig],
[
'module:react-native-dotenv',
{
-21
View File
@@ -1,21 +0,0 @@
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]
}
-9
View File
@@ -1,9 +0,0 @@
{
"name": "react-compiler-runtime",
"version": "0.0.1",
"license": "MIT",
"main": "index.js",
"peerDependencies": {
"react": "^18.2.0"
}
}
+2 -1
View File
@@ -38,9 +38,10 @@ cfg.transformer.getTransformOptions = async () => ({
inlineRequires: true,
nonInlinedRequires: [
// We can remove this option and rely on the default after
// https://github.com/facebook/metro/pull/1126 is released.
// https://github.com/facebook/metro/pull/1390 is released.
'React',
'react',
'react-compiler-runtime',
'react/jsx-dev-runtime',
'react/jsx-runtime',
'react-native',
+5 -5
View File
@@ -166,14 +166,14 @@
"postinstall-postinstall": "^2.1.0",
"psl": "^1.9.0",
"react": "18.2.0",
"react-compiler-runtime": "file:./lib/react-compiler-runtime",
"react-compiler-runtime": "^19.0.0-beta-a7bf2bd-20241110",
"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.0-alpha.3",
"react-native-drawer-layout": "^4.0.1",
"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": "^0.0.0-experimental-592953e-20240517",
"babel-plugin-react-compiler": "^19.0.0-beta-a7bf2bd-20241110",
"babel-plugin-react-native-web": "^0.18.12",
"babel-preset-expo": "^10.0.0",
"eslint": "^8.19.0",
@@ -251,9 +251,10 @@
"eslint-plugin-ft-flow": "^2.0.3",
"eslint-plugin-lingui": "^0.2.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-compiler": "^0.0.0-experimental-c8b3f72-20240517",
"eslint-plugin-react-compiler": "^19.0.0-beta-a7bf2bd-20241110",
"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",
@@ -265,7 +266,6 @@
"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",
+125 -12
View File
@@ -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..288e60c 100644
index b09e653..d2b4e05 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..288e60c 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..288e60c 100644
@@ -203,4 +210,58 @@ - (void)refreshControlValueChanged
}
}
+- (void)setCustomTintColor:(UIColor *)customTintColor
+{
+ _customTintColor = customTintColor;
@@ -139,26 +139,139 @@ 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();
@@ -166,5 +279,5 @@ index 5f5e1ab..aac00b6 100644
+ if (currentRunnable != null) {
+ currentRunnable.cancel();
}
mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
+9
View File
@@ -0,0 +1,9 @@
'use strict'
module.exports = {
target: '18',
environment: {
enableTreatRefLikeIdentifiersAsRefs: true,
validateRefAccessDuringRender: false, // TODO: Make it `true`.
},
}
+40 -33
View File
@@ -56,7 +56,6 @@ 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)
@@ -81,40 +80,40 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return {
transform: [
{
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
},
{
scale: interpolate(
outroLogo.value,
outroLogo.get(),
[0, 0.08, 1],
[1, 0.8, 500],
'clamp',
),
},
],
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
}
})
const bottomLogoAnimation = useAnimatedStyle(() => {
return {
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
}
})
const reducedLogoAnimation = useAnimatedStyle(() => {
return {
transform: [
{
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
},
],
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
}
})
const logoWrapperAnimation = useAnimatedStyle(() => {
return {
opacity: interpolate(
outroAppOpacity.value,
outroAppOpacity.get(),
[0, 0.1, 0.2, 1],
[1, 1, 0, 0],
'clamp',
@@ -126,11 +125,11 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return {
transform: [
{
scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'),
scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'),
},
],
opacity: interpolate(
outroAppOpacity.value,
outroAppOpacity.get(),
[0, 0.1, 0.2, 1],
[0, 0, 1, 1],
'clamp',
@@ -146,29 +145,37 @@ export function Splash(props: React.PropsWithChildren<Props>) {
if (isReady) {
SplashScreen.hideAsync()
.then(() => {
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),
})
},
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),
}),
)
},
),
)
})
.catch(() => {})
+3 -3
View File
@@ -29,10 +29,10 @@ export function useDialogControl(): DialogOuterProps['control'] {
const {activeDialogs} = useDialogStateContext()
React.useEffect(() => {
activeDialogs.current.set(id, control)
const map = activeDialogs.current
map.set(id, control)
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
activeDialogs.current.delete(id)
map.delete(id)
}
}, [id, activeDialogs])
+3 -4
View File
@@ -17,13 +17,12 @@ export function Loader(props: Props) {
const rotation = useSharedValue(0)
const animatedStyles = useAnimatedStyle(() => ({
transform: [{rotate: rotation.value + 'deg'}],
transform: [{rotate: rotation.get() + 'deg'}],
}))
React.useEffect(() => {
rotation.value = withRepeat(
withTiming(360, {duration: 500, easing: Easing.linear}),
-1,
rotation.set(() =>
withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1),
)
}, [rotation])
+28 -22
View File
@@ -55,13 +55,15 @@ export const ProgressGuideToast = React.forwardRef<
// animate the opacity then set isOpen to false when done
const setIsntOpen = () => setIsOpen(false)
opacity.value = withTiming(
0,
{
duration: 400,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(setIsntOpen)(),
opacity.set(() =>
withTiming(
0,
{
duration: 400,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(setIsntOpen)(),
),
)
}, [setIsOpen, opacity])
@@ -71,20 +73,24 @@ export const ProgressGuideToast = React.forwardRef<
// animate the vertical translation, the opacity, and the checkmark
const playCheckmark = () => animatedCheckRef.current?.play()
opacity.value = 0
opacity.value = withTiming(
1,
{
duration: 100,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(playCheckmark)(),
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,
easing: Easing.out(Easing.cubic),
}),
)
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)
@@ -114,8 +120,8 @@ export const ProgressGuideToast = React.forwardRef<
}, [winDim.width])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{translateY: translateY.value}],
opacity: opacity.value,
transform: [{translateY: translateY.get()}],
opacity: opacity.get(),
}))
return (
+12 -8
View File
@@ -32,21 +32,25 @@ export const AnimatedCheck = React.forwardRef<
const checkAnim = useSharedValue(0)
const circleAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: 166 - circleAnim.value * 166,
strokeDashoffset: 166 - circleAnim.get() * 166,
}))
const checkAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: 48 - 48 * checkAnim.value,
strokeDashoffset: 48 - 48 * checkAnim.get(),
}))
const play = React.useCallback(
(cb?: () => void) => {
circleAnim.value = 0
checkAnim.value = 0
circleAnim.set(0)
checkAnim.set(0)
circleAnim.value = withTiming(1, {duration: 500, easing: Easing.linear})
checkAnim.value = withDelay(
500,
withTiming(1, {duration: 300, easing: Easing.linear}, cb),
circleAnim.set(() =>
withTiming(1, {duration: 500, easing: Easing.linear}),
)
checkAnim.set(() =>
withDelay(
500,
withTiming(1, {duration: 300, easing: Easing.linear}, cb),
),
)
},
[circleAnim, checkAnim],
+45 -22
View File
@@ -146,18 +146,17 @@ export function Inner({
<ErrorMessage message={error} />
</View>
) : null}
<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>{' '}
{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>
?{' '}
<InlineLinkText
to="#"
label={_(msg`Change email address`)}
@@ -169,17 +168,41 @@ export function Inner({
})
return false
}}>
<Trans>Need to change it?</Trans>
<Trans>Change your email address</Trans>
</InlineLinkText>
</>
) : (
reasonText
)}
</>
) : (
uiStrings[currentStep].message
)}
</Text>
.
</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>
)}
</View>
{currentStep === 'StepTwo' ? (
<View>
@@ -1,117 +0,0 @@
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>
)
}
+11 -16
View File
@@ -19,7 +19,6 @@ 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'
@@ -36,19 +35,7 @@ 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,
@@ -66,7 +53,14 @@ export function NuxDialogs() {
const onboardingActive = useOnboardingState().isActive
const isLoading =
!currentAccount || !preferences || !profile || onboardingActive
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.
return !isLoading ? (
<Inner
currentAccount={currentAccount}
@@ -106,6 +100,7 @@ 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])
@@ -174,7 +169,7 @@ function Inner({
return (
<Context.Provider value={ctx}>
{activeNux === Nux.NeueTypography && <NeueTypography />}
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
</Context.Provider>
)
}
+9 -7
View File
@@ -34,7 +34,7 @@ export function ActionsWrapper({
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
transform: [{scale: scale.get()}],
}))
const open = React.useCallback(() => {
@@ -46,7 +46,7 @@ export function ActionsWrapper({
const shrink = React.useCallback(() => {
'worklet'
cancelAnimation(scale)
scale.value = withTiming(1, {duration: 200})
scale.set(() => withTiming(1, {duration: 200}))
}, [scale])
const doubleTapGesture = Gesture.Tap()
@@ -58,11 +58,13 @@ export function ActionsWrapper({
const pressAndHoldGesture = Gesture.LongPress()
.onStart(() => {
'worklet'
scale.value = withTiming(1.05, {duration: 200}, finished => {
if (!finished) return
runOnJS(open)()
shrink()
})
scale.set(() =>
withTiming(1.05, {duration: 200}, finished => {
if (!finished) return
runOnJS(open)()
shrink()
}),
)
})
.onTouchesUp(shrink)
.onTouchesMove(shrink)
+3 -3
View File
@@ -42,12 +42,12 @@ export function ChatEmptyPill() {
const onPressIn = React.useCallback(() => {
if (isWeb) return
scale.value = withTiming(1.075, {duration: 100})
scale.set(() => withTiming(1.075, {duration: 100}))
}, [scale])
const onPressOut = React.useCallback(() => {
if (isWeb) return
scale.value = withTiming(1, {duration: 100})
scale.set(() => 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.value}],
transform: [{scale: scale.get()}],
}))
return (
+1 -1
View File
@@ -62,7 +62,7 @@ export let MessageMenu = ({
message.text,
langPrefs.primaryLanguage,
)
openLink(translatorUrl)
openLink(translatorUrl, true)
}, [langPrefs.primaryLanguage, message.text, openLink])
const onDelete = React.useCallback(() => {
+3 -3
View File
@@ -35,12 +35,12 @@ export function NewMessagesPill({
const onPressIn = React.useCallback(() => {
if (isWeb) return
scale.value = withTiming(1.075, {duration: 100})
scale.set(() => withTiming(1.075, {duration: 100}))
}, [scale])
const onPressOut = React.useCallback(() => {
if (isWeb) return
scale.value = withTiming(1, {duration: 100})
scale.set(() => withTiming(1, {duration: 100}))
}, [scale])
const onPress = React.useCallback(() => {
@@ -49,7 +49,7 @@ export function NewMessagesPill({
}, [onPressInner, playHaptic])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
transform: [{scale: scale.get()}],
}))
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,6 +313,7 @@ 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"
/>
+5
View File
@@ -0,0 +1,5 @@
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,6 +68,8 @@ 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_md,
a.gap_sm,
a.px_lg,
a.py_lg,
a.justify_between,
@@ -74,10 +74,9 @@ export function Buttons({
hideLabel?: string
}) {
const {_} = useLingui()
const {gtPhone} = useBreakpoints()
return (
<View style={[{minHeight: 35}, gtPhone ? undefined : a.w_full]}>
<View style={[{minHeight: 35}, a.w_full]}>
<ToggleButton.Group
label={_(
msg`Configure content filtering setting for category: ${name}`,
@@ -259,7 +258,7 @@ export function LabelerLabelPreference({
</Content>
{showConfig && (
<View style={[gtPhone ? undefined : a.w_full]}>
<>
{cantConfigure ? (
<View
style={[
@@ -290,7 +289,7 @@ export function LabelerLabelPreference({
hideLabel={hideLabel}
/>
)}
</View>
</>
)}
</Outer>
)
+1 -1
View File
@@ -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 = 650000 // estimate as of 10/28/24
export const JOINED_THIS_WEEK = 3500000 // estimate as of 11/16/24
export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at
+43 -41
View File
@@ -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.value, min, max)
return clamp(transX.get(), min, max)
})
const iconScale = useSharedValue(1)
@@ -75,21 +75,23 @@ export function GestureActionView({
return
}
iconScale.value = withSequence(
withTiming(1.2, {duration: 175}),
withTiming(1, {duration: 100}),
iconScale.set(() =>
withSequence(
withTiming(1.2, {duration: 175}),
withTiming(1, {duration: 100}),
),
)
}
useAnimatedReaction(
() => transX,
() => {
if (transX.value === 0) {
if (transX.get() === 0) {
runOnJS(setActiveAction)(null)
} else if (transX.value < 0) {
} else if (transX.get() < 0) {
if (
actions.leftSecond &&
transX.value <= -actions.leftSecond.threshold
transX.get() <= -actions.leftSecond.threshold
) {
if (activeAction !== 'leftSecond') {
runOnJS(setActiveAction)('leftSecond')
@@ -97,10 +99,10 @@ export function GestureActionView({
} else if (activeAction !== 'leftFirst') {
runOnJS(setActiveAction)('leftFirst')
}
} else if (transX.value > 0) {
} else if (transX.get() > 0) {
if (
actions.rightSecond &&
transX.value > actions.rightSecond.threshold
transX.get() > actions.rightSecond.threshold
) {
if (activeAction !== 'rightSecond') {
runOnJS(setActiveAction)('rightSecond')
@@ -119,44 +121,44 @@ export function GestureActionView({
.activeOffsetY([-200, 200])
.onStart(() => {
'worklet'
isActive.value = true
isActive.set(true)
})
.onChange(e => {
'worklet'
transX.value = e.translationX
transX.set(e.translationX)
if (e.translationX < 0) {
// Left side
if (actions.leftSecond) {
if (
e.translationX <= -actions.leftSecond.threshold &&
!hitSecond.value
!hitSecond.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitSecond.value = true
hitSecond.set(true)
} else if (
hitSecond.value &&
hitSecond.get() &&
e.translationX > -actions.leftSecond.threshold
) {
runPopAnimation()
hitSecond.value = false
hitSecond.set(false)
}
}
if (!hitSecond.value && actions.leftFirst) {
if (!hitSecond.get() && actions.leftFirst) {
if (
e.translationX <= -actions.leftFirst.threshold &&
!hitFirst.value
!hitFirst.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitFirst.value = true
hitFirst.set(true)
} else if (
hitFirst.value &&
hitFirst.get() &&
e.translationX > -actions.leftFirst.threshold
) {
hitFirst.value = false
hitFirst.set(false)
}
}
} else if (e.translationX > 0) {
@@ -164,33 +166,33 @@ export function GestureActionView({
if (actions.rightSecond) {
if (
e.translationX >= actions.rightSecond.threshold &&
!hitSecond.value
!hitSecond.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitSecond.value = true
hitSecond.set(true)
} else if (
hitSecond.value &&
hitSecond.get() &&
e.translationX < actions.rightSecond.threshold
) {
runPopAnimation()
hitSecond.value = false
hitSecond.set(false)
}
}
if (!hitSecond.value && actions.rightFirst) {
if (!hitSecond.get() && actions.rightFirst) {
if (
e.translationX >= actions.rightFirst.threshold &&
!hitFirst.value
!hitFirst.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitFirst.value = true
hitFirst.set(true)
} else if (
hitFirst.value &&
hitFirst.get() &&
e.translationX < actions.rightFirst.threshold
) {
hitFirst.value = false
hitFirst.set(false)
}
}
}
@@ -198,29 +200,29 @@ export function GestureActionView({
.onEnd(e => {
'worklet'
if (e.translationX < 0) {
if (hitSecond.value && actions.leftSecond) {
if (hitSecond.get() && actions.leftSecond) {
runOnJS(actions.leftSecond.action)()
} else if (hitFirst.value && actions.leftFirst) {
} else if (hitFirst.get() && actions.leftFirst) {
runOnJS(actions.leftFirst.action)()
}
} else if (e.translationX > 0) {
if (hitSecond.value && actions.rightSecond) {
if (hitSecond.get() && actions.rightSecond) {
runOnJS(actions.rightSecond.action)()
} else if (hitSecond.value && actions.rightFirst) {
} else if (hitSecond.get() && actions.rightFirst) {
runOnJS(actions.rightFirst.action)()
}
}
transX.value = withTiming(0, {duration: 200})
hitFirst.value = false
hitSecond.value = false
isActive.value = false
transX.set(() => withTiming(0, {duration: 200}))
hitFirst.set(false)
hitSecond.set(false)
isActive.set(false)
})
const composedGesture = Gesture.Simultaneous(panGesture)
const animatedSliderStyle = useAnimatedStyle(() => {
return {
transform: [{translateX: clampedTransX.value}],
transform: [{translateX: clampedTransX.get()}],
}
})
@@ -274,7 +276,7 @@ export function GestureActionView({
const animatedBackgroundStyle = useAnimatedStyle(() => {
return {
backgroundColor: interpolateColor(
clampedTransX.value,
clampedTransX.get(),
interpolation.inputRange,
// @ts-expect-error - Weird type expected by reanimated, but this is okay
interpolation.outputRange,
@@ -283,10 +285,10 @@ export function GestureActionView({
})
const animatedIconStyle = useAnimatedStyle(() => {
const absTransX = Math.abs(clampedTransX.value)
const absTransX = Math.abs(clampedTransX.get())
return {
opacity: interpolate(absTransX, [0, 75], [0.15, 1]),
transform: [{scale: iconScale.value}],
transform: [{scale: iconScale.get()}],
}
})
+5 -8
View File
@@ -2,7 +2,6 @@ import React from 'react'
import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
import Animated, {
cancelAnimation,
runOnJS,
useAnimatedStyle,
useReducedMotion,
useSharedValue,
@@ -32,27 +31,25 @@ export function PressableScale({
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
transform: [{scale: scale.get()}],
}))
return (
<AnimatedPressable
accessibilityRole="button"
onPressIn={e => {
'worklet'
if (onPressIn) {
runOnJS(onPressIn)(e)
onPressIn(e)
}
cancelAnimation(scale)
scale.value = withTiming(targetScale, {duration: 100})
scale.set(() => withTiming(targetScale, {duration: 100}))
}}
onPressOut={e => {
'worklet'
if (onPressOut) {
runOnJS(onPressOut)(e)
onPressOut(e)
}
cancelAnimation(scale)
scale.value = withTiming(1, {duration: 100})
scale.set(() => withTiming(1, {duration: 100}))
}}
style={[!reducedMotion && animatedStyle, style]}
{...rest}>
+15 -2
View File
@@ -1,4 +1,5 @@
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'
@@ -7,13 +8,25 @@ 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 && !currentAccount?.emailConfirmed
const needsEmailVerification =
!isSelfHost &&
checkEmailConfirmed &&
!currentAccount?.emailConfirmed &&
isNewEnough
return {needsEmailVerification}
}
+11 -9
View File
@@ -10,15 +10,16 @@ export function useMinimalShellHeaderTransform() {
const {headerHeight} = useShellLayout()
const headerTransform = useAnimatedStyle(() => {
const headerModeValue = headerMode.get()
return {
pointerEvents: headerMode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - headerMode.value, 2),
pointerEvents: headerModeValue === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - headerModeValue, 2),
transform: [
{
translateY: interpolate(
headerMode.value,
headerModeValue,
[0, 1],
[0, -headerHeight.value],
[0, -headerHeight.get()],
),
},
],
@@ -33,15 +34,16 @@ export function useMinimalShellFooterTransform() {
const {footerHeight} = useShellLayout()
const footerTransform = useAnimatedStyle(() => {
const footerModeValue = footerMode.get()
return {
pointerEvents: footerMode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - footerMode.value, 2),
pointerEvents: footerModeValue === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - footerModeValue, 2),
transform: [
{
translateY: interpolate(
footerMode.value,
footerModeValue,
[0, 1],
[0, footerHeight.value],
[0, footerHeight.get()],
),
},
],
@@ -58,7 +60,7 @@ export function useMinimalShellFabTransform() {
return {
transform: [
{
translateY: interpolate(footerMode.value, [0, 1], [-44, 0]),
translateY: interpolate(footerMode.get(), [0, 1], [-44, 0]),
},
],
}
-32
View File
@@ -1,32 +0,0 @@
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]
}
+13 -9
View File
@@ -9,20 +9,24 @@ if ('scrollRestoration' in history) {
function createInitialScrollState() {
return {
scrollYs: new Map(),
focusedKey: null as string | null,
// 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,
},
}
}
export function useWebScrollRestoration() {
const [state] = useState(createInitialScrollState)
const [ref] = useState(createInitialScrollState)
const navigation = useNavigation()
useEffect(() => {
function onDispatch() {
if (state.focusedKey) {
if (ref.current.focusedKey) {
// Remember where we were for later.
state.scrollYs.set(state.focusedKey, window.scrollY)
ref.current.scrollYs.set(ref.current.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.
@@ -36,17 +40,17 @@ export function useWebScrollRestoration() {
return () => {
navigation.removeListener('__unsafe_action__' as any, onDispatch)
}
}, [state, navigation])
}, [ref, navigation])
const screenListeners = useMemo(
() => ({
focus(e: EventArg<'focus', boolean | undefined, unknown>) {
const scrollY = state.scrollYs.get(e.target) ?? 0
const scrollY = ref.current.scrollYs.get(e.target) ?? 0
window.scrollTo(0, scrollY)
state.focusedKey = e.target ?? null
ref.current.focusedKey = e.target ?? null
},
}),
[state],
[ref],
)
return screenListeners
}
+10 -4
View File
@@ -1,3 +1,5 @@
import {t} from '@lingui/macro'
export function cleanError(str: any): string {
if (!str) {
return ''
@@ -6,13 +8,17 @@ export function cleanError(str: any): string {
str = str.toString()
}
if (isNetworkError(str)) {
return 'Unable to connect. Please check your internet connection and try again.'
return t`Unable to connect. Please check your internet connection and try again.`
}
if (str.includes('Upstream Failure')) {
return 'The server appears to be experiencing issues. Please try again in a few moments.'
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('Bad token scope')) {
return 'This feature is not available while using an App Password. Please sign in with your main password.'
return t`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)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
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.value - topInset - 150
const max = windowHeight - -keyboardHeight.get() - topInset - 150
const availableSpace = max - measurement.height
maxHeight.value = max
isInputScrollable.value = availableSpace < 30
maxHeight.set(max)
isInputScrollable.set(availableSpace < 30)
},
},
[windowHeight, topInset],
)
const animatedStyle = useAnimatedStyle(() => ({
maxHeight: maxHeight.value,
maxHeight: maxHeight.get(),
}))
const animatedProps = useAnimatedProps(() => ({
scrollEnabled: isInputScrollable.value,
scrollEnabled: isInputScrollable.get(),
}))
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.value && hasScrolled) {
if (isWeb && isAtTop.get() && 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.value) {
if (height > 50 && isAtBottom.get()) {
// 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.value - 50 &&
height - prevContentHeight.current > layoutHeight.get() - 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.value) {
if (hasScrolled && prevContentHeight.current > layoutHeight.get()) {
convoState.fetchMessageHistory()
}
}, [convoState, hasScrolled, layoutHeight])
@@ -217,18 +217,18 @@ export function MessagesList({
const onScroll = React.useCallback(
(e: ReanimatedScrollEvent) => {
'worklet'
layoutHeight.value = e.layoutMeasurement.height
layoutHeight.set(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.value = e.contentSize.height - 100 < bottomOffset
isAtTop.value = e.contentOffset.y <= 1
isAtBottom.set(e.contentSize.height - 100 < bottomOffset)
isAtTop.set(e.contentOffset.y <= 1)
if (
newMessagesPill.show &&
(e.contentOffset.y > newMessagesPill.startContentOffset + 200 ||
isAtBottom.value)
isAtBottom.get())
) {
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.value = true
keyboardHeight.value = e.height
layoutScrollWithoutAnimation.set(true)
keyboardHeight.set(e.height)
} else {
keyboardIsOpening.value = true
keyboardIsOpening.set(true)
}
},
onMove: e => {
'worklet'
keyboardHeight.value = e.height
keyboardHeight.set(e.height)
if (e.height > bottomOffset) {
scrollTo(flatListRef, 0, 1e7, false)
}
},
onEnd: () => {
'worklet'
keyboardIsOpening.value = false
keyboardIsOpening.set(false)
},
})
const animatedListStyle = useAnimatedStyle(() => ({
marginBottom:
keyboardHeight.value > bottomOffset ? keyboardHeight.value : bottomOffset,
keyboardHeight.get() > bottomOffset ? keyboardHeight.get() : bottomOffset,
}))
// -- Message sending
@@ -363,13 +363,13 @@ export function MessagesList({
// -- List layout changes (opening emoji keyboard, etc.)
const onListLayout = React.useCallback(
(e: LayoutChangeEvent) => {
layoutHeight.value = e.nativeEvent.layout.height
layoutHeight.set(e.nativeEvent.layout.height)
if (isWeb || !keyboardIsOpening.value) {
if (isWeb || !keyboardIsOpening.get()) {
flatListRef.current?.scrollToEnd({
animated: !layoutScrollWithoutAnimation.value,
animated: !layoutScrollWithoutAnimation.get(),
})
layoutScrollWithoutAnimation.value = false
layoutScrollWithoutAnimation.set(false)
}
},
[
+23 -18
View File
@@ -127,31 +127,36 @@ export function StepFinished() {
})(),
(async () => {
const {imageUri, imageMime} = profileStepResults
if (imageUri && imageMime) {
const blobPromise = uploadBlob(agent, imageUri, imageMime)
await agent.upsertProfile(async existing => {
existing = existing ?? {}
const blobPromise =
imageUri && imageMime
? uploadBlob(agent, imageUri, imageMime)
: undefined
await agent.upsertProfile(async existing => {
existing = existing ?? {}
if (blobPromise) {
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.value, [-150, 0], [1.2, 1], {
scale: interpolate(scrollY.get(), [-150, 0], [1.2, 1], {
extrapolateRight: Extrapolation.CLAMP,
}),
},
@@ -66,7 +66,7 @@ function GrowableBannerInner({
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{
scale: interpolate(scrollY.value, [-150, 0], [2, 1], {
scale: interpolate(scrollY.get(), [-150, 0], [2, 1], {
extrapolateRight: Extrapolation.CLAMP,
}),
},
@@ -76,7 +76,7 @@ function GrowableBannerInner({
const animatedBlurViewProps = useAnimatedProps(() => {
return {
intensity: interpolate(
scrollY.value,
scrollY.get(),
[-300, -65, -15],
[50, 40, 0],
Extrapolation.CLAMP,
@@ -85,16 +85,17 @@ function GrowableBannerInner({
})
const animatedSpinnerStyle = useAnimatedStyle(() => {
const scrollYValue = scrollY.get()
return {
display: scrollY.value < 0 ? 'flex' : 'none',
display: scrollYValue < 0 ? 'flex' : 'none',
opacity: interpolate(
scrollY.value,
scrollYValue,
[-60, -15],
[1, 0],
Extrapolation.CLAMP,
),
transform: [
{translateY: interpolate(scrollY.value, [-150, 0], [-75, 0])},
{translateY: interpolate(scrollYValue, [-150, 0], [-75, 0])},
{rotate: '90deg'},
],
}
@@ -103,7 +104,7 @@ function GrowableBannerInner({
const animatedBackButtonStyle = useAnimatedStyle(() => ({
transform: [
{
translateY: interpolate(scrollY.value, [-150, 60], [-150, 60], {
translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], {
extrapolateRight: Extrapolation.CLAMP,
}),
},
@@ -168,7 +169,7 @@ function useShouldAnimateSpinner({
const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10)
useAnimatedReaction(
() => scrollY.value < -5,
() => scrollY.get() < -5,
(value, prevValue) => {
if (value !== prevValue) {
runOnJS(setIsOverscrolled)(value)
+2
View File
@@ -80,6 +80,8 @@ 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)
}
+2 -2
View File
@@ -1,5 +1,5 @@
export const ACTIVE_POLL_INTERVAL = 3e3
export const MESSAGE_SCREEN_POLL_INTERVAL = 10e3
export const ACTIVE_POLL_INTERVAL = 4e3
export const MESSAGE_SCREEN_POLL_INTERVAL = 30e3
export const BACKGROUND_POLL_INTERVAL = 60e3
export const INACTIVE_TIMEOUT = 60e3 * 5
+1 -1
View File
@@ -1,2 +1,2 @@
export const DEFAULT_POLL_INTERVAL = 60e3 * 5
export const DEFAULT_POLL_INTERVAL = 60e3
export const BACKGROUND_POLL_INTERVAL = 60e3 * 5
+2
View File
@@ -173,6 +173,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return wasActive
})
// FIXME
// eslint-disable-next-line react-compiler/react-compiler
unstable__openModal = openModal
unstable__closeModal = closeModal
+2 -2
View File
@@ -18,14 +18,14 @@ const RQKEY_ROOT = 'actor-autocomplete'
export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
export function useActorAutocompleteQuery(
prefix: string,
rawPrefix: string,
maintainData?: boolean,
limit?: number,
) {
const moderationOpts = useModerationOpts()
const agent = useAgent()
prefix = prefix.toLowerCase().trim()
let prefix = rawPrefix.toLowerCase().trim()
if (prefix.endsWith('.')) {
// Going from "foo" to "foo." should not clear matches.
prefix = prefix.slice(0, -1)
+36 -3
View File
@@ -5,7 +5,11 @@ 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 {RQKEY as LIST_CONVOS_KEY} from './list-converations'
import {
ConvoListQueryData,
getConvoFromQueryData,
RQKEY as LIST_CONVOS_KEY,
} from './list-converations'
const RQKEY_ROOT = 'convo'
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
@@ -57,8 +61,37 @@ export function useMarkAsReadMutation() {
if (!convoId) throw new Error('No convoId provided')
optimisticUpdate(convoId)
},
onSettled() {
queryClient.invalidateQueries({queryKey: LIST_CONVOS_KEY})
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.
}
})
},
})
}
@@ -39,7 +39,7 @@ export function useListConvosQuery({
queryKey: RQKEY,
queryFn: async ({pageParam}) => {
const {data} = await agent.api.chat.bsky.convo.listConvos(
{cursor: pageParam},
{cursor: pageParam, limit: 20},
{headers: DM_SERVICE_HEADERS},
)
@@ -47,9 +47,6 @@ 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,
})
}
@@ -180,6 +177,11 @@ 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()
}
})
@@ -245,12 +247,12 @@ export function useUnreadMessageCount() {
return useMemo(() => {
return {
count,
numUnread: count > 0 ? (count > 30 ? '30+' : String(count)) : undefined,
numUnread: count > 0 ? (count > 10 ? '10+' : String(count)) : undefined,
}
}, [count])
}
type ConvoListQueryData = {
export type ConvoListQueryData = {
pageParams: Array<string | undefined>
pages: Array<ChatBskyConvoListConvos.OutputSchema>
}
@@ -301,7 +303,7 @@ function optimisticDelete(chatId: string, old: ConvoListQueryData) {
}
}
function getConvoFromQueryData(chatId: string, old: ConvoListQueryData) {
export function getConvoFromQueryData(chatId: string, old: ConvoListQueryData) {
for (const page of old.pages) {
for (const convo of page.convos) {
if (convo.id === chatId) {
+16 -15
View File
@@ -8,6 +8,7 @@ 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'
@@ -50,7 +51,6 @@ 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,19 +70,6 @@ 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) => {
@@ -190,7 +177,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
},
}
}, [setNumUnread, queryClient, moderationOpts, agent])
checkUnreadRef.current = api.checkUnread
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])
return (
<stateContext.Provider value={numUnread}>
+11
View File
@@ -216,6 +216,17 @@ 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
+2
View File
@@ -187,6 +187,8 @@ 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,
+10 -6
View File
@@ -44,13 +44,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
'worklet'
// Cancel any existing animation
cancelAnimation(headerMode)
headerMode.value = withSpring(v ? 1 : 0, {
overshootClamping: true,
})
headerMode.set(() =>
withSpring(v ? 1 : 0, {
overshootClamping: true,
}),
)
cancelAnimation(footerMode)
footerMode.value = withSpring(v ? 1 : 0, {
overshootClamping: true,
})
footerMode.set(() =>
withSpring(v ? 1 : 0, {
overshootClamping: true,
}),
)
},
[headerMode, footerMode],
)
+6 -3
View File
@@ -61,14 +61,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const {mutateAsync, variables, isPending} =
useSetActiveProgressGuideMutation()
const activeProgressGuide = (
let 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.numLikes = Number(activeProgressGuide.numLikes) || 0
activeProgressGuide.numFollows = Number(activeProgressGuide.numFollows) || 0
activeProgressGuide = {
...activeProgressGuide,
numLikes: Number(activeProgressGuide.numLikes) || 0,
numFollows: Number(activeProgressGuide.numFollows) || 0,
}
}
const [localGuideState, setLocalGuideState] =
+30 -20
View File
@@ -130,6 +130,7 @@ import {
ThreadDraft,
} from './state/composer'
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
import {clearThumbnailCache} from './videos/VideoTranscodeBackdrop'
type CancelRef = {
onPressCancel: () => void
@@ -249,7 +250,8 @@ export const ComposePost = ({
const onClose = useCallback(() => {
closeComposer()
}, [closeComposer])
clearThumbnailCache(queryClient)
}, [closeComposer, queryClient])
const insets = useSafeAreaInsets()
const viewStyles = useMemo(
@@ -1265,12 +1267,12 @@ function useScrollTracker({
const contentHeight = useSharedValue(0)
const hasScrolledToTop = useDerivedValue(() =>
withTiming(contentOffset.value === 0 ? 1 : 0),
withTiming(contentOffset.get() === 0 ? 1 : 0),
)
const hasScrolledToBottom = useDerivedValue(() =>
withTiming(
contentHeight.value - contentOffset.value - 5 <= scrollViewHeight.value
contentHeight.get() - contentOffset.get() - 5 <= scrollViewHeight.get()
? 1
: 0,
),
@@ -1288,11 +1290,11 @@ function useScrollTracker({
}) => {
'worklet'
if (typeof newContentHeight === 'number')
contentHeight.value = Math.floor(newContentHeight)
contentHeight.set(Math.floor(newContentHeight))
if (typeof newContentOffset === 'number')
contentOffset.value = Math.floor(newContentOffset)
contentOffset.set(Math.floor(newContentOffset))
if (typeof newScrollViewHeight === 'number')
scrollViewHeight.value = Math.floor(newScrollViewHeight)
scrollViewHeight.set(Math.floor(newScrollViewHeight))
},
[contentHeight, contentOffset, scrollViewHeight],
)
@@ -1308,21 +1310,22 @@ function useScrollTracker({
},
})
const onScrollViewContentSizeChange = useCallback(
(_width: number, height: number) => {
if (stickyBottom && height > contentHeight.value) {
const onScrollViewContentSizeChangeUIThread = useCallback(
(newContentHeight: number) => {
'worklet'
const oldContentHeight = contentHeight.get()
let shouldScrollToBottom = false
if (stickyBottom && newContentHeight > oldContentHeight) {
const isFairlyCloseToBottom =
contentHeight.value - contentOffset.value - 100 <=
scrollViewHeight.value
oldContentHeight - contentOffset.get() - 100 <= scrollViewHeight.get()
if (isFairlyCloseToBottom) {
runOnUI(() => {
scrollTo(scrollViewRef, 0, contentHeight.value, true)
})()
shouldScrollToBottom = true
}
}
showHideBottomBorder({
newContentHeight: height,
})
showHideBottomBorder({newContentHeight})
if (shouldScrollToBottom) {
scrollTo(scrollViewRef, 0, newContentHeight, true)
}
},
[
showHideBottomBorder,
@@ -1334,6 +1337,13 @@ function useScrollTracker({
],
)
const onScrollViewContentSizeChange = useCallback(
(_width: number, height: number) => {
runOnUI(onScrollViewContentSizeChangeUIThread)(height)
},
[onScrollViewContentSizeChangeUIThread],
)
const onScrollViewLayout = useCallback(
(evt: LayoutChangeEvent) => {
showHideBottomBorder({
@@ -1347,7 +1357,7 @@ function useScrollTracker({
return {
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor(
hasScrolledToTop.value,
hasScrolledToTop.get(),
[0, 1],
[t.atoms.border_contrast_medium.borderColor, 'transparent'],
),
@@ -1357,7 +1367,7 @@ function useScrollTracker({
return {
borderTopWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor(
hasScrolledToBottom.value,
hasScrolledToBottom.get(),
[0, 1],
[t.atoms.border_contrast_medium.borderColor, 'transparent'],
),
@@ -1602,7 +1612,7 @@ function VideoUploadToolbar({state}: {state: VideoState}) {
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{rotateZ: `${rotate.value}deg`}],
transform: [{rotateZ: `${rotate.get()}deg`}],
}
})
+20 -18
View File
@@ -158,22 +158,26 @@ function DialogInner({
<Toggle.Item name="porn" label={_(msg`Porn`)}>
<Toggle.Checkbox />
<Toggle.LabelText>
<Trans>Porn</Trans>
<Trans>Adult</Trans>
</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
<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>
{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}
</View>
</View>
<View>
@@ -203,16 +207,14 @@ function DialogInner({
</Toggle.LabelText>
</Toggle.Item>
</Toggle.Group>
<Text style={[a.mt_sm, t.atoms.text_contrast_medium]}>
{labels.includes('graphic-media') ? (
{labels.includes('graphic-media') ? (
<Text style={[a.mt_sm, t.atoms.text_contrast_medium]}>
<Trans>
Media that may be disturbing or inappropriate for some
audiences.
</Trans>
) : (
<Trans>Does not contain graphic or disturbing content.</Trans>
)}
</Text>
</Text>
) : null}
</View>
</View>
</View>
@@ -303,6 +303,8 @@ 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,25 +1,26 @@
import React, {useEffect} from 'react'
import React from 'react'
import {clearCache, createVideoThumbnail} from 'react-native-compressor'
import Animated, {FadeIn} from 'react-native-reanimated'
import {Image} from 'expo-image'
import {useQuery} from '@tanstack/react-query'
import {QueryClient, 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: ['thumbnail', uri],
queryKey: [RQKEY, uri],
queryFn: async () => {
return await createVideoThumbnail(uri)
},
})
useEffect(() => {
return () => {
clearCache()
}
}, [])
return (
thumbnail && (
<Animated.View style={a.flex_1} entering={FadeIn}>
@@ -1,3 +1,7 @@
export function clearThumbnailCache() {
// no-op
}
export function VideoTranscodeBackdrop() {
return null
}
+1 -1
View File
@@ -93,7 +93,7 @@ function HomeHeaderLayoutDesktopAndTablet({
{tabBarAnchor}
<Animated.View
onLayout={e => {
headerHeight.value = e.nativeEvent.layout.height
headerHeight.set(e.nativeEvent.layout.height)
}}
style={[
t.atoms.bg,
+1 -1
View File
@@ -43,7 +43,7 @@ export function HomeHeaderLayoutMobile({
<Animated.View
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
onLayout={e => {
headerHeight.value = e.nativeEvent.layout.height
headerHeight.set(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.value !== 1) {
if (pinchScale.get() !== 1) {
// We're currently pinching.
return true
}
const [, , committedScale] = readTransform(committedTransform.value)
const [, , committedScale] = readTransform(committedTransform.get())
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.value = {
pinchOrigin.set({
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.value)
const [, , committedScale] = readTransform(committedTransform.get())
const maxCommittedScale = Math.max(
MIN_SCREEN_ZOOM,
(imageDimensions.width / screenSize.width) * MAX_ORIGINAL_IMAGE_ZOOM,
@@ -171,20 +171,21 @@ const ImageItem = ({
Math.max(minPinchScale, e.scale),
maxPinchScale,
)
pinchScale.value = nextPinchScale
pinchScale.set(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.value)
prependPinch(t, nextPinchScale, pinchOrigin.value, pinchTranslation.value)
prependTransform(t, committedTransform.value)
prependPan(t, panTranslation.get())
prependPinch(t, nextPinchScale, pinchOrigin.get(), pinchTranslation.get())
prependTransform(t, committedTransform.get())
const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize)
if (dx !== 0 || dy !== 0) {
pinchTranslation.value = {
x: pinchTranslation.value.x + dx,
y: pinchTranslation.value.y + dy,
}
const pt = pinchTranslation.get()
pinchTranslation.set({
x: pt.x + dx,
y: pt.y + dy,
})
}
})
.onEnd(() => {
@@ -193,18 +194,18 @@ const ImageItem = ({
let t = createTransform()
prependPinch(
t,
pinchScale.value,
pinchOrigin.value,
pinchTranslation.value,
pinchScale.get(),
pinchOrigin.get(),
pinchTranslation.get(),
)
prependTransform(t, committedTransform.value)
prependTransform(t, committedTransform.get())
applyRounding(t)
committedTransform.value = t
committedTransform.set(t)
// Reset just the pinch.
pinchScale.value = 1
pinchOrigin.value = {x: 0, y: 0}
pinchTranslation.value = {x: 0, y: 0}
pinchScale.set(1)
pinchOrigin.set({x: 0, y: 0})
pinchTranslation.set({x: 0, y: 0})
})
const pan = Gesture.Pan()
@@ -223,29 +224,29 @@ const ImageItem = ({
prependPan(t, nextPanTranslation)
prependPinch(
t,
pinchScale.value,
pinchOrigin.value,
pinchTranslation.value,
pinchScale.get(),
pinchOrigin.get(),
pinchTranslation.get(),
)
prependTransform(t, committedTransform.value)
prependTransform(t, committedTransform.get())
// Prevent panning from going out of bounds.
const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize)
nextPanTranslation.x += dx
nextPanTranslation.y += dy
panTranslation.value = nextPanTranslation
panTranslation.set(nextPanTranslation)
})
.onEnd(() => {
'worklet'
// Commit just the pan.
let t = createTransform()
prependPan(t, panTranslation.value)
prependTransform(t, committedTransform.value)
prependPan(t, panTranslation.get())
prependTransform(t, committedTransform.get())
applyRounding(t)
committedTransform.value = t
committedTransform.set(t)
// Reset just the pan.
panTranslation.value = {x: 0, y: 0}
panTranslation.set({x: 0, y: 0})
})
const singleTap = Gesture.Tap().onEnd(() => {
@@ -261,11 +262,11 @@ const ImageItem = ({
if (!imageDimensions || !imageAspect) {
return
}
const [, , committedScale] = readTransform(committedTransform.value)
const [, , committedScale] = readTransform(committedTransform.get())
if (committedScale !== 1) {
// Go back to 1:1 using the identity vector.
let t = createTransform()
committedTransform.value = withClampedSpring(t)
committedTransform.set(withClampedSpring(t))
return
}
@@ -299,7 +300,7 @@ const ImageItem = ({
)
const finalTransform = createTransform()
prependPinch(finalTransform, scale, origin, {x: dx, y: dy})
committedTransform.value = withClampedSpring(finalTransform)
committedTransform.set(withClampedSpring(finalTransform))
})
const composedGesture = isScrollViewBeingDragged
@@ -313,13 +314,13 @@ const ImageItem = ({
)
const containerStyle = useAnimatedStyle(() => {
const {scaleAndMoveTransform, isHidden} = transforms.value
const {scaleAndMoveTransform, isHidden} = transforms.get()
// 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.value)
prependPinch(t, pinchScale.value, pinchOrigin.value, pinchTranslation.value)
prependTransform(t, committedTransform.value)
prependPan(t, panTranslation.get())
prependPinch(t, pinchScale.get(), pinchOrigin.get(), pinchTranslation.get())
prependTransform(t, committedTransform.get())
const [translateX, translateY, scale] = readTransform(t)
const manipulationTransform = [
{translateX},
@@ -338,7 +339,7 @@ const ImageItem = ({
})
const imageCropStyle = useAnimatedStyle(() => {
const {cropFrameTransform} = transforms.value
const {cropFrameTransform} = transforms.get()
return {
flex: 1,
overflow: 'hidden',
@@ -347,7 +348,7 @@ const ImageItem = ({
})
const imageStyle = useAnimatedStyle(() => {
const {cropContentTransform} = transforms.value
const {cropContentTransform} = transforms.get()
return {
flex: 1,
transform: cropContentTransform,
@@ -359,7 +360,7 @@ const ImageItem = ({
const [hasLoaded, setHasLoaded] = useState(false)
useAnimatedReaction(
() => {
return transforms.value.isResting && !hasLoaded
return transforms.get().isResting && !hasLoaded
},
(show, prevShow) => {
if (show && !prevShow) {
@@ -148,7 +148,7 @@ const ImageItem = ({
)
const containerStyle = useAnimatedStyle(() => {
const {scaleAndMoveTransform, isHidden} = transforms.value
const {scaleAndMoveTransform, isHidden} = transforms.get()
return {
flex: 1,
transform: scaleAndMoveTransform,
@@ -158,7 +158,7 @@ const ImageItem = ({
const imageCropStyle = useAnimatedStyle(() => {
const screenSize = measureSafeArea()
const {cropFrameTransform} = transforms.value
const {cropFrameTransform} = transforms.get()
return {
overflow: 'hidden',
transform: cropFrameTransform,
@@ -171,7 +171,7 @@ const ImageItem = ({
})
const imageStyle = useAnimatedStyle(() => {
const {cropContentTransform} = transforms.value
const {cropContentTransform} = transforms.get()
return {
transform: cropContentTransform,
width: '100%',
@@ -184,7 +184,7 @@ const ImageItem = ({
const [hasLoaded, setHasLoaded] = useState(false)
useAnimatedReaction(
() => {
return transforms.value.isResting && !hasLoaded
return transforms.get().isResting && !hasLoaded
},
(show, prevShow) => {
if (show && !prevShow) {
+58 -38
View File
@@ -32,6 +32,7 @@ import Animated, {
useSharedValue,
withDecay,
withSpring,
WithSpringConfig,
} from 'react-native-reanimated'
import {
Edge,
@@ -62,8 +63,18 @@ const EDGES =
? (['top', 'bottom', 'left', 'right'] satisfies Edge[])
: (['left', 'right'] satisfies Edge[]) // iOS, so no top/bottom safe area
const SLOW_SPRING = {stiffness: 120}
const FAST_SPRING = {stiffness: 700}
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,
}
export default function ImageViewRoot({
lightbox: nextLightbox,
@@ -76,7 +87,6 @@ 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)
@@ -98,18 +108,22 @@ export default function ImageViewRoot({
// https://github.com/software-mansion/react-native-reanimated/issues/6677
requestAnimationFrame(() => {
openProgress.value = canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1
openProgress.set(() =>
canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1,
)
})
return () => {
// https://github.com/software-mansion/react-native-reanimated/issues/6677
requestAnimationFrame(() => {
openProgress.value = canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0
openProgress.set(() =>
canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0,
)
})
}
}, [nextLightbox, openProgress])
useAnimatedReaction(
() => openProgress.value === 0,
() => openProgress.get() === 0,
(isGone, wasGone) => {
if (isGone && !wasGone) {
runOnJS(setActiveLightbox)(null)
@@ -119,7 +133,7 @@ export default function ImageViewRoot({
const onFlyAway = React.useCallback(() => {
'worklet'
openProgress.value = 0
openProgress.set(0)
runOnJS(onRequestClose)()
}, [onRequestClose, openProgress])
@@ -176,7 +190,7 @@ function ImageView({
const isFlyingAway = useSharedValue(false)
const containerStyle = useAnimatedStyle(() => {
if (openProgress.value < 1 || isFlyingAway.value) {
if (openProgress.get() < 1 || isFlyingAway.get()) {
return {pointerEvents: 'none'}
}
return {pointerEvents: 'auto'}
@@ -185,11 +199,12 @@ function ImageView({
const backdropStyle = useAnimatedStyle(() => {
const screenSize = measure(safeAreaRef)
let opacity = 1
if (openProgress.value < 1) {
opacity = Math.sqrt(openProgress.value)
const openProgressValue = openProgress.get()
if (openProgressValue < 1) {
opacity = Math.sqrt(openProgressValue)
} else if (screenSize) {
const dragProgress = Math.min(
Math.abs(dismissSwipeTranslateY.value) / (screenSize.height / 2),
Math.abs(dismissSwipeTranslateY.get()) / (screenSize.height / 2),
1,
)
opacity -= dragProgress
@@ -201,11 +216,11 @@ function ImageView({
})
const animatedHeaderStyle = useAnimatedStyle(() => {
const show = showControls && dismissSwipeTranslateY.value === 0
const show = showControls && dismissSwipeTranslateY.get() === 0
return {
pointerEvents: show ? 'box-none' : 'none',
opacity: withClampedSpring(
show && openProgress.value === 1 ? 1 : 0,
show && openProgress.get() === 1 ? 1 : 0,
FAST_SPRING,
),
transform: [
@@ -216,12 +231,12 @@ function ImageView({
}
})
const animatedFooterStyle = useAnimatedStyle(() => {
const show = showControls && dismissSwipeTranslateY.value === 0
const show = showControls && dismissSwipeTranslateY.get() === 0
return {
flexGrow: 1,
pointerEvents: show ? 'box-none' : 'none',
opacity: withClampedSpring(
show && openProgress.value === 1 ? 1 : 0,
show && openProgress.get() === 1 ? 1 : 0,
FAST_SPRING,
),
transform: [
@@ -248,7 +263,7 @@ function ImageView({
const screenSize = measure(safeAreaRef)
return (
!screenSize ||
Math.abs(dismissSwipeTranslateY.value) > screenSize.height
Math.abs(dismissSwipeTranslateY.get()) > screenSize.height
)
},
(isOut, wasOut) => {
@@ -386,10 +401,11 @@ function LightboxImage({
const transforms = useDerivedValue(() => {
'worklet'
const safeArea = measureSafeArea()
const openProgressValue = openProgress.get()
const dismissTranslateY =
isActive && openProgress.value === 1 ? dismissSwipeTranslateY.value : 0
isActive && openProgressValue === 1 ? dismissSwipeTranslateY.get() : 0
if (openProgress.value === 0 && isFlyingAway.value) {
if (openProgressValue === 0 && isFlyingAway.get()) {
return {
isHidden: true,
isResting: false,
@@ -399,9 +415,9 @@ function LightboxImage({
}
}
if (isActive && thumbRect && imageAspect && openProgress.value < 1) {
if (isActive && thumbRect && imageAspect && openProgressValue < 1) {
return interpolateTransform(
openProgress.value,
openProgressValue,
thumbRect,
safeArea,
imageAspect,
@@ -423,33 +439,37 @@ function LightboxImage({
.maxPointers(1)
.onUpdate(e => {
'worklet'
if (openProgress.value !== 1 || isFlyingAway.value) {
if (openProgress.get() !== 1 || isFlyingAway.get()) {
return
}
dismissSwipeTranslateY.value = e.translationY
dismissSwipeTranslateY.set(e.translationY)
})
.onEnd(e => {
'worklet'
if (openProgress.value !== 1 || isFlyingAway.value) {
if (openProgress.get() !== 1 || isFlyingAway.get()) {
return
}
if (Math.abs(e.velocityY) > 1000) {
isFlyingAway.value = true
if (dismissSwipeTranslateY.value === 0) {
if (Math.abs(e.velocityY) > 200) {
isFlyingAway.set(true)
if (dismissSwipeTranslateY.get() === 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.value = 1
dismissSwipeTranslateY.set(1)
}
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.
})
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.
}),
)
} else {
dismissSwipeTranslateY.value = withSpring(0, {
stiffness: 700,
damping: 50,
})
dismissSwipeTranslateY.set(() =>
withSpring(0, {
stiffness: 700,
damping: 50,
}),
)
}
})
@@ -706,7 +726,7 @@ function interpolateTransform(
}
}
function withClampedSpring(value: any, {stiffness}: {stiffness: number}) {
function withClampedSpring(value: any, config: WithSpringConfig) {
'worklet'
return withSpring(value, {overshootClamping: true, stiffness})
return withSpring(value, {...config, overshootClamping: true})
}
+6 -6
View File
@@ -131,11 +131,11 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
const lastForcedScrollY = useSharedValue(0)
const adjustScrollForOtherPages = () => {
'worklet'
const currentScrollY = scrollY.value
const currentScrollY = scrollY.get()
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
if (lastForcedScrollY.value !== forcedScrollY) {
lastForcedScrollY.value = forcedScrollY
const refs = scrollRefs.value
if (lastForcedScrollY.get() !== forcedScrollY) {
lastForcedScrollY.set(forcedScrollY)
const refs = scrollRefs.get()
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.value = nextScrollY
scrollY.set(nextScrollY)
runOnJS(queueThrottledOnScroll)()
}
},
@@ -246,7 +246,7 @@ let PagerTabBar = ({
allowHeaderOverScroll?: boolean
}): React.ReactNode => {
const headerTransform = useAnimatedStyle(() => {
const translateY = Math.min(scrollY.value, headerOnlyHeight) * -1
const translateY = Math.min(scrollY.get(), headerOnlyHeight) * -1
return {
transform: [
{
+188 -87
View File
@@ -1,5 +1,5 @@
import React, {memo, useMemo} from 'react'
import {StyleSheet, View} from 'react-native'
import {StyleSheet, Text as RNText, View} from 'react-native'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
@@ -8,7 +8,6 @@ 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'
@@ -21,6 +20,7 @@ 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,26 +28,31 @@ 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 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'
import {Text} from '#/components/Typography'
import {WhoCanReply} from '#/components/WhoCanReply'
export function PostThreadItem({
post,
@@ -125,19 +130,20 @@ export function PostThreadItem({
}
function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) {
const pal = usePalette('default')
const t = useTheme()
return (
<View
style={[
styles.outer,
pal.border,
pal.view,
s.p20,
s.flexRow,
hideTopBorder && styles.noTopBorder,
t.atoms.bg,
t.atoms.border_contrast_low,
a.p_xl,
a.pl_lg,
a.flex_row,
a.gap_md,
!hideTopBorder && a.border_t,
]}>
<FontAwesomeIcon icon={['far', 'trash-can']} color={pal.colors.icon} />
<Text style={[pal.textLight, s.ml10]}>
<TrashIcon style={[t.atoms.text]} />
<Text style={[t.atoms.text_contrast_medium, a.mt_2xs]}>
<Trans>This post has been deleted.</Trans>
</Text>
</View>
@@ -308,7 +314,7 @@ let PostThreadItemLoaded = ({
/>
<View style={[a.flex_1]}>
<Link style={s.flex1} href={authorHref} title={authorTitle}>
<NewText
<Text
emoji
style={[a.text_lg, a.font_bold, a.leading_snug, a.self_start]}
numberOfLines={1}>
@@ -317,10 +323,10 @@ let PostThreadItemLoaded = ({
sanitizeHandle(post.author.handle),
moderation.ui('displayName'),
)}
</NewText>
</Text>
</Link>
<Link style={s.flex1} href={authorHref} title={authorTitle}>
<NewText
<Text
emoji
style={[
a.text_md,
@@ -329,7 +335,7 @@ let PostThreadItemLoaded = ({
]}
numberOfLines={1}>
{sanitizeHandle(post.author.handle, '@')}
</NewText>
</Text>
</Link>
</View>
{currentAccount?.did !== post.author.did && (
@@ -393,48 +399,48 @@ let PostThreadItemLoaded = ({
]}>
{post.repostCount != null && post.repostCount !== 0 ? (
<Link href={repostsHref} title={repostsTitle}>
<NewText
<Text
testID="repostCount-expanded"
style={[a.text_md, t.atoms.text_contrast_medium]}>
<NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{formatCount(i18n, post.repostCount)}
</NewText>{' '}
</Text>{' '}
<Plural
value={post.repostCount}
one="repost"
other="reposts"
/>
</NewText>
</Text>
</Link>
) : null}
{post.quoteCount != null &&
post.quoteCount !== 0 &&
!post.viewer?.embeddingDisabled ? (
<Link href={quotesHref} title={quotesTitle}>
<NewText
<Text
testID="quoteCount-expanded"
style={[a.text_md, t.atoms.text_contrast_medium]}>
<NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{formatCount(i18n, post.quoteCount)}
</NewText>{' '}
</Text>{' '}
<Plural
value={post.quoteCount}
one="quote"
other="quotes"
/>
</NewText>
</Text>
</Link>
) : null}
{post.likeCount != null && post.likeCount !== 0 ? (
<Link href={likesHref} title={likesTitle}>
<NewText
<Text
testID="likeCount-expanded"
style={[a.text_md, t.atoms.text_contrast_medium]}>
<NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{formatCount(i18n, post.likeCount)}
</NewText>{' '}
</Text>{' '}
<Plural value={post.likeCount} one="like" other="likes" />
</NewText>
</Text>
</Link>
) : null}
</View>
@@ -617,13 +623,13 @@ let PostThreadItemLoaded = ({
href={postHref}
title={itemTitle}
noFeedback>
<Text type="sm-medium" style={pal.textLight}>
<Text
style={[t.atoms.text_contrast_medium, a.font_bold, a.text_sm]}>
<Trans>More</Trans>
</Text>
<FontAwesomeIcon
icon="angle-right"
color={pal.colors.textLight}
size={14}
<ChevronRightIcon
size="xs"
style={[t.atoms.text_contrast_medium]}
/>
</Link>
) : undefined}
@@ -651,26 +657,24 @@ function PostOuterWrapper({
hideTopBorder?: boolean
}>) {
const t = useTheme()
const [hover, setHover] = React.useState(false)
const {
state: hover,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
if (treeView && depth > 0) {
return (
<View
style={[
a.flex_row,
a.px_sm,
a.flex_row,
t.atoms.border_contrast_low,
styles.cursor,
{
flexDirection: 'row',
borderTopWidth: depth === 1 ? a.border_t.borderTopWidth : 0,
},
depth === 1 && a.border_t,
]}
onPointerEnter={() => {
setHover(true)
}}
onPointerLeave={() => {
setHover(false)
}}>
onPointerEnter={onHoverIn}
onPointerLeave={onHoverOut}>
{Array.from(Array(depth - 1)).map((_, n: number) => (
<View
key={`${post.uri}-padding-${n}`}
@@ -684,18 +688,23 @@ function PostOuterWrapper({
]}
/>
))}
<View style={{flex: 1}}>{children}</View>
<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>
)
}
return (
<View
onPointerEnter={() => {
setHover(true)
}}
onPointerLeave={() => {
setHover(false)
}}
onPointerEnter={onHoverIn}
onPointerLeave={onHoverOut}
style={[
a.border_t,
a.px_sm,
@@ -728,36 +737,128 @@ function ExpandedPostDetails({
const isRootPost = !('reply' in post.record)
const onTranslatePress = React.useCallback(() => {
openLink(translatorUrl)
openLink(translatorUrl, true)
}, [openLink, translatorUrl])
return (
<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]}>
&middot;
</NewText>
<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]}>
&middot;
</Text>
<InlineLinkText
to="#"
label={_(msg`Translate`)}
style={[a.text_sm, pal.link]}
onPress={onTranslatePress}>
<Trans>Translate</Trans>
</InlineLinkText>
</>
)}
<InlineLinkText
to="#"
label={_(msg`Translate`)}
style={[a.text_sm, pal.link]}
onPress={onTranslatePress}>
<Trans>Translate</Trans>
</InlineLinkText>
</>
)}
</View>
</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.value, // current snap index
animatedIndex.get(), // current snap index
[-1, 0], // input range
[0, 0.5], // output range
Extrapolation.CLAMP,
+80 -60
View File
@@ -18,6 +18,7 @@ import {
useNavigationDeduped,
} from '#/lib/hooks/useNavigationDeduped'
import {useOpenLink} from '#/lib/hooks/useOpenLink'
import {getTabState, TabState} from '#/lib/routes/helpers'
import {
convertBskyAppUrlIfNeeded,
isExternalUrl,
@@ -25,6 +26,7 @@ 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'
@@ -127,15 +129,15 @@ export const Link = memo(function Link({
)
}
let dataSet = props.dataSet
if (anchorNoUnderline) {
// @ts-ignore web only -prf
props.dataSet = props.dataSet || {}
// @ts-ignore web only -prf
props.dataSet.noUnderline = 1
dataSet = {...dataSet, noUnderline: 1}
}
if (title && !props.accessibilityLabel) {
props.accessibilityLabel = title
let accessibilityLabel = props.accessibilityLabel
if (title && !accessibilityLabel) {
accessibilityLabel = title
}
const Com = props.hoverStyle ? PressableWithHover : Pressable
@@ -148,7 +150,11 @@ export const Link = memo(function Link({
accessibilityRole="link"
// @ts-ignore web only -prf
href={anchorHref}
{...props}>
{...props}
// @ts-ignore web only
dataSet={dataSet}
accessibilityLabel={accessibilityLabel}
accessibilityHint={props.accessibilityHint}>
{children ? children : <Text>{title || 'link'}</Text>}
</Com>
)
@@ -162,7 +168,7 @@ export const TextLink = memo(function TextLink({
text,
numberOfLines,
lineHeight,
dataSet,
dataSet: rawDataSet,
title,
onPress,
onBeforePress,
@@ -185,7 +191,7 @@ export const TextLink = memo(function TextLink({
anchorNoUnderline?: boolean
onBeforePress?: () => void
} & TextProps) {
const {...props} = useLinkProps({to: sanitizeUrl(href)})
let {...props} = useLinkProps({to: sanitizeUrl(href)})
const navigation = useNavigationDeduped()
const {openModal, closeModal} = useModalControls()
const openLink = useOpenLink()
@@ -194,61 +200,68 @@ export const TextLink = memo(function TextLink({
console.error('Unable to detect mismatching label')
}
let dataSet = rawDataSet
if (anchorNoUnderline) {
dataSet = dataSet ?? {}
dataSet.noUnderline = 1
dataSet = {
...dataSet,
noUnderline: 1,
}
}
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(
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,
closeModal,
openModal,
navigation,
sanitizeUrl(href),
href,
text,
disableMismatchWarning,
navigationAction,
openLink,
e,
)
},
[
onBeforePress,
onPress,
closeModal,
openModal,
navigation,
href,
text,
disableMismatchWarning,
navigationAction,
openLink,
],
)
],
),
}
const hrefAttrs = useMemo(() => {
const isExternal = isExternalUrl(href)
if (isExternal) {
@@ -400,15 +413,22 @@ 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(...router.matchPath(href)))
navigation.dispatch(StackActions.push(routeName, params))
} else if (navigationAction === 'replace') {
// @ts-ignore we're not able to type check on this one -prf
navigation.dispatch(StackActions.replace(...router.matchPath(href)))
navigation.dispatch(StackActions.replace(routeName, params))
} else if (navigationAction === 'navigate') {
// @ts-ignore we're not able to type check on this one -prf
navigation.navigate(...router.matchPath(href))
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)
}
} else {
throw Error('Unsupported navigator action.')
}
+5 -2
View File
@@ -8,6 +8,7 @@ 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'
@@ -52,6 +53,7 @@ function ListImpl<ItemT>(
const isScrolledDown = useSharedValue(false)
const t = useTheme()
const dedupe = useDedupe(400)
const {activeLightbox} = useLightbox()
function handleScrolledDownChange(didScrollDown: boolean) {
onScrolledDownChange?.(didScrollDown)
@@ -77,8 +79,8 @@ function ListImpl<ItemT>(
onScrollFromContext?.(e, ctx)
const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
if (isScrolledDown.value !== didScrollDown) {
isScrolledDown.value = didScrollDown
if (isScrolledDown.get() !== didScrollDown) {
isScrolledDown.set(didScrollDown)
if (onScrolledDownChange != null) {
runOnJS(handleScrolledDownChange)(didScrollDown)
}
@@ -143,6 +145,7 @@ function ListImpl<ItemT>(
contentOffset={contentOffset}
refreshControl={refreshControl}
onScroll={scrollHandler}
scrollsToTop={!activeLightbox}
scrollEventThrottle={1}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
+27 -24
View File
@@ -44,7 +44,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
(v: boolean) => {
'worklet'
cancelAnimation(headerMode)
headerMode.value = v ? V1.value : V0.value
headerMode.set(v ? V1.get() : V0.get())
},
[headerMode],
)
@@ -52,9 +52,9 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
useEffect(() => {
if (isWeb) {
return listenToForcedWindowScroll(() => {
startDragOffset.value = null
startMode.value = null
didJustRestoreScroll.value = true
startDragOffset.set(null)
startMode.set(null)
didJustRestoreScroll.set(true)
})
}
})
@@ -63,13 +63,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
(e: NativeScrollEvent) => {
'worklet'
if (isNative) {
if (startDragOffset.value === null) {
const startDragOffsetValue = startDragOffset.get()
if (startDragOffsetValue === null) {
return
}
const didScrollDown = e.contentOffset.y > startDragOffset.value
startDragOffset.value = null
startMode.value = null
if (e.contentOffset.y < headerHeight.value) {
const didScrollDown = e.contentOffset.y > startDragOffsetValue
startDragOffset.set(null)
startMode.set(null)
if (e.contentOffset.y < headerHeight.get()) {
// If we're close to the top, show the shell.
setMode(false)
} else if (didScrollDown) {
@@ -77,7 +78,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
setMode(true)
} else {
// Snap to whichever state is the closest.
setMode(Math.round(headerMode.value) === 1)
setMode(Math.round(headerMode.get()) === 1)
}
}
},
@@ -88,8 +89,8 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
(e: NativeScrollEvent) => {
'worklet'
if (isNative) {
startDragOffset.value = e.contentOffset.y
startMode.value = headerMode.value
startDragOffset.set(e.contentOffset.y)
startMode.set(headerMode.get())
}
},
[headerMode, startDragOffset, startMode],
@@ -123,10 +124,12 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
(e: NativeScrollEvent) => {
'worklet'
if (isNative) {
if (startDragOffset.value === null || startMode.value === null) {
const startDragOffsetValue = startDragOffset.get()
const startModeValue = startMode.get()
if (startDragOffsetValue === null || startModeValue === null) {
if (
headerMode.value !== 0 &&
e.contentOffset.y < headerHeight.value
headerMode.get() !== 0 &&
e.contentOffset.y < headerHeight.get()
) {
// If we're close enough to the top, always show the shell.
// Even if we're not dragging.
@@ -137,29 +140,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 - startDragOffset.value
const dy = e.contentOffset.y - startDragOffsetValue
const dProgress = interpolate(
dy,
[-headerHeight.value, headerHeight.value],
[-headerHeight.get(), headerHeight.get()],
[-1, 1],
)
const newValue = clamp(startMode.value + dProgress, 0, 1)
if (newValue !== headerMode.value) {
const newValue = clamp(startModeValue + dProgress, 0, 1)
if (newValue !== headerMode.get()) {
// Manually adjust the value. This won't be (and shouldn't be) animated.
// Cancel any any existing animation
cancelAnimation(headerMode)
headerMode.value = newValue
headerMode.set(newValue)
}
} else {
if (didJustRestoreScroll.value) {
didJustRestoreScroll.value = false
if (didJustRestoreScroll.get()) {
didJustRestoreScroll.set(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.value ?? 0)
startDragOffset.value = e.contentOffset.y
const dy = e.contentOffset.y - (startDragOffset.get() ?? 0)
startDragOffset.set(e.contentOffset.y)
if (dy < 0 || e.contentOffset.y < WEB_HIDE_SHELL_THRESHOLD) {
setMode(false)
+8 -4
View File
@@ -96,6 +96,8 @@ 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
@@ -114,10 +116,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
// @ts-ignore web only -prf
props.dataSet = props.dataSet || {}
// @ts-ignore web only -prf
props.dataSet.stableGutters = '1'
dataSet = {
...dataSet,
stableGutters: '1',
}
}
}
return (
@@ -131,6 +133,8 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
style={style}
contentOffset={contentOffset}
{...props}
// @ts-ignore web only -prf
dataSet={dataSet}
/>
)
})
+315 -284
View File
@@ -102,12 +102,77 @@ let PostDropdownBtn = ({
timestamp: string
threadgateRecord?: AppBskyFeedThreadgate.Record
}): React.ReactNode => {
const {hasSession, currentAccount} = useSession()
const theme = useTheme()
const alf = useAlf()
const {gtMobile} = useBreakpoints()
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 langPrefs = useLanguagePrefs()
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
const {mutateAsync: pinPostMutate, isPending: isPinPending} =
@@ -241,7 +306,7 @@ let PostDropdownBtn = ({
}, [_, richText])
const onPressTranslate = React.useCallback(async () => {
await openLink(translatorUrl)
await openLink(translatorUrl, true)
}, [openLink, translatorUrl])
const onHidePost = React.useCallback(() => {
@@ -360,308 +425,276 @@ let PostDropdownBtn = ({
}, [isPinned, pinPostMutate, postCid, postUri])
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>
{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 />
</>
)}
<>
{isAuthor && (
<>
<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()
}
}}>
testID="pinPostBtn"
label={
isPinned
? _(msg`Unpin from profile`)
: _(msg`Pin to your profile`)
}
disabled={isPinPending}
onPress={onPressPin}>
<Menu.ItemText>
{isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
{isPinned
? _(msg`Unpin from profile`)
: _(msg`Pin to your profile`)}
</Menu.ItemText>
<Menu.ItemIcon icon={Share} position="right" />
<Menu.ItemIcon
icon={isPinPending ? Loader : PinIcon}
position="right"
/>
</Menu.Item>
</Menu.Group>
<Menu.Divider />
</>
)}
<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>
{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.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 && 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>
{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="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>
</>
)}
<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 && (
<>
<Menu.Divider />
<Menu.Group>
{hasSession &&
(canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
<>
<Menu.Divider />
<Menu.Group>
{canHidePostForMe && (
<Menu.Item
testID="postDropdownMuteThreadBtn"
testID="postDropdownHideBtn"
label={
isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
isReply
? _(msg`Hide reply for me`)
: _(msg`Hide post for me`)
}
onPress={onToggleThreadMute}>
onPress={() => hidePromptControl.open()}>
<Menu.ItemText>
{isThreadMuted
? _(msg`Unmute thread`)
: _(msg`Mute thread`)}
{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={isThreadMuted ? Unmute : Mute}
icon={isReplyHiddenByThreadgate ? Eye : EyeSlash}
position="right"
/>
</Menu.Item>
)}
{canDetachQuote && (
<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" />
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>
</>
)}
)}
</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>
)}
{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>
{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>
</>
)}
<Prompt.Basic
control={deletePromptControl}
@@ -745,9 +778,7 @@ let PostDropdownBtn = ({
onConfirm={onToggleReplyVisibility}
confirmButtonCta={_(msg`Yes, hide`)}
/>
</EventStopper>
</>
)
}
PostDropdownBtn = memo(PostDropdownBtn)
export {PostDropdownBtn}
PostDropdownMenuItems = memo(PostDropdownMenuItems)
+38 -31
View File
@@ -122,9 +122,8 @@ let DrawerProfileCard = ({
DrawerProfileCard = React.memo(DrawerProfileCard)
export {DrawerProfileCard}
let DrawerContent = ({}: {}): React.ReactNode => {
let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
const t = useTheme()
const {_} = useLingui()
const insets = useSafeAreaInsets()
const setDrawerOpen = useSetDrawerOpen()
const navigation = useNavigation<NavigationProp>()
@@ -137,7 +136,6 @@ let DrawerContent = ({}: {}): React.ReactNode => {
isAtMessages,
} = useNavigationTabState()
const {hasSession, currentAccount} = useSession()
const kawaii = useKawaiiMode()
// events
// =
@@ -277,34 +275,7 @@ let DrawerContent = ({}: {}): React.ReactNode => {
<View style={[a.px_xl]}>
<Divider style={[a.mb_xl, a.mt_sm]} />
<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>
<ExtraLinks />
</View>
</ScrollView>
@@ -633,3 +604,39 @@ 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>
)
}
+1 -1
View File
@@ -134,7 +134,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
footerMinimalShellTransform,
]}
onLayout={e => {
footerHeight.value = e.nativeEvent.layout.height
footerHeight.set(e.nativeEvent.layout.height)
}}>
{hasSession ? (
<>
+1
View File
@@ -105,6 +105,7 @@ function ShellInner() {
onOpen={onOpenDrawer}
onClose={onCloseDrawer}
swipeEdgeWidth={winDim.width / 2}
drawerType={isIOS ? 'slide' : 'front'}
swipeEnabled={!canGoBack && hasSession && !isDrawerSwipeDisabled}
overlayStyle={{
backgroundColor: select(t.name, {
+173 -3008
View File
File diff suppressed because it is too large Load Diff