diff --git a/.github/workflows/build-and-push-embedr-aws.yaml b/.github/workflows/build-and-push-embedr-aws.yaml index f7f24af9f7..23d1538fe5 100644 --- a/.github/workflows/build-and-push-embedr-aws.yaml +++ b/.github/workflows/build-and-push-embedr-aws.yaml @@ -3,8 +3,6 @@ on: push: branches: - main - - bnewbold/embedr - - bnewbold/embedr-rebase env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 22cc657353..f0e23263db 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,6 +16,10 @@ jobs: steps: - name: Check out Git repository uses: actions/checkout@v3 + - name: Install node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc - name: Yarn install uses: Wandalen/wretry.action@master with: diff --git a/.nvmrc b/.nvmrc index 3c032078a4..209e3ef4b6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +20 diff --git a/Dockerfile b/Dockerfile index 50bdd18cb9..557321872a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /usr/src/social-app ENV DEBIAN_FRONTEND=noninteractive # Node -ENV NODE_VERSION=18 +ENV NODE_VERSION=20 ENV NVM_DIR=/usr/share/nvm # Go diff --git a/Dockerfile.embedr b/Dockerfile.embedr index 663cbcfc51..3623b3293e 100644 --- a/Dockerfile.embedr +++ b/Dockerfile.embedr @@ -5,7 +5,7 @@ WORKDIR /usr/src/social-app ENV DEBIAN_FRONTEND=noninteractive # Node -ENV NODE_VERSION=18 +ENV NODE_VERSION=20 ENV NVM_DIR=/usr/share/nvm # Go diff --git a/__e2e__/flows/composer-self-label.yml b/__e2e__/flows/composer-self-label.yml index c89e46db55..26aa0fab01 100644 --- a/__e2e__/flows/composer-self-label.yml +++ b/__e2e__/flows/composer-self-label.yml @@ -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" diff --git a/__e2e__/flows/create-account.yml b/__e2e__/flows/create-account.yml index 2bb2d06459..99ac1371a5 100644 --- a/__e2e__/flows/create-account.yml +++ b/__e2e__/flows/create-account.yml @@ -29,7 +29,6 @@ appId: xyz.blueskyweb.app - pressKey: Enter - tapOn: id: "nextBtn" -- tapOn: "Not now" - tapOn: id: "handleInput" - inputText: "e2e-test" diff --git a/__e2e__/flows/curate-lists.yml b/__e2e__/flows/curate-lists.yml index d46c58eed0..662ec84233 100644 --- a/__e2e__/flows/curate-lists.yml +++ b/__e2e__/flows/curate-lists.yml @@ -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" diff --git a/__e2e__/flows/login.yml b/__e2e__/flows/login.yml index 34fe634c56..80e61a3ba0 100644 --- a/__e2e__/flows/login.yml +++ b/__e2e__/flows/login.yml @@ -23,5 +23,4 @@ appId: xyz.blueskyweb.app id: "loginPasswordInput" - inputText: "hunter2" - pressKey: Enter -- tapOn: "Not now" - assertVisible: "Following" diff --git a/__e2e__/flows/mod-lists.yml b/__e2e__/flows/mod-lists.yml index 1d6bb2da26..54832a07eb 100644 --- a/__e2e__/flows/mod-lists.yml +++ b/__e2e__/flows/mod-lists.yml @@ -22,6 +22,7 @@ appId: xyz.blueskyweb.app id: "editDescriptionInput" - inputText: "Shhh" - tapOn: "Save" +- tapOn: "Save" # view modlist - assertVisible: "Muted Users" diff --git a/__e2e__/flows/onboarding.yml b/__e2e__/flows/onboarding.yml index 68d9897885..88b51b39b8 100644 --- a/__e2e__/flows/onboarding.yml +++ b/__e2e__/flows/onboarding.yml @@ -13,7 +13,7 @@ appId: xyz.blueskyweb.app - tapOn: "Select an avatar" - waitForAnimationToEnd - tapOn: - point: "16%,22%" + point: "50%,22%" - waitForAnimationToEnd - tapOn: "Choose" - waitForAnimationToEnd diff --git a/assets/icons/calendarClock_stroke2_corner0_rounded.svg b/assets/icons/calendarClock_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..e6535e0861 --- /dev/null +++ b/assets/icons/calendarClock_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/bskyweb/cmd/embedr/server.go b/bskyweb/cmd/embedr/server.go index 904b4df9a2..a9a8ab57f9 100644 --- a/bskyweb/cmd/embedr/server.go +++ b/bskyweb/cmd/embedr/server.go @@ -112,8 +112,8 @@ func serve(cctx *cli.Context) error { Skipper: middleware.DefaultSkipper, Store: middleware.NewRateLimiterMemoryStoreWithConfig( middleware.RateLimiterMemoryStoreConfig{ - Rate: 10, // requests per second - Burst: 30, // allow bursts + Rate: 20, // requests per second + Burst: 150, // allow bursts ExpiresIn: 3 * time.Minute, // garbage collect entries older than 3 minutes }, ), diff --git a/package.json b/package.json index 379d21a93f..33a92819ad 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.94.0", "private": true, "engines": { - "node": ">=18" + "node": ">=20" }, "packageManager": "yarn@1.22.19", "scripts": { @@ -103,7 +103,7 @@ "@tiptap/suggestion": "^2.6.6", "@types/invariant": "^2.2.37", "@types/lodash.throttle": "^4.1.9", - "@types/node": "^18.16.2", + "@types/node": "^20.14.3", "@zxing/text-encoding": "^0.9.0", "array.prototype.findlast": "^1.2.3", "await-lock": "^2.2.2", @@ -173,7 +173,7 @@ "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", diff --git a/patches/react-native+0.74.1.patch b/patches/react-native+0.74.1.patch index ea6161e2ff..d9f55aa308 100644 --- a/patches/react-native+0.74.1.patch +++ b/patches/react-native+0.74.1.patch @@ -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 *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 *)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); diff --git a/src/components/Error.tsx b/src/components/Error.tsx index a27ccb88c6..2819986b34 100644 --- a/src/components/Error.tsx +++ b/src/components/Error.tsx @@ -32,7 +32,7 @@ export function Error({ return ( { + if (needsEmailVerification) { + verifyEmailControl.open() + } else { + confirmDialogControl.open() + } + }} style={{backgroundColor: 'transparent'}}> Make one for me @@ -262,7 +273,13 @@ function Empty() { color="primary" size="small" disabled={isGenerating} - onPress={() => navigation.navigate('StarterPackWizard')} + onPress={() => { + if (needsEmailVerification) { + verifyEmailControl.open() + } else { + navigation.navigate('StarterPackWizard') + } + }} style={{ backgroundColor: 'white', borderColor: 'white', @@ -318,6 +335,12 @@ function Empty() { onConfirm={generate} confirmButtonCta={_(msg`Retry`)} /> + ) } diff --git a/src/components/SubtleWebHover.tsx b/src/components/SubtleWebHover.tsx index e6f427237b..5cbbfc898a 100644 --- a/src/components/SubtleWebHover.tsx +++ b/src/components/SubtleWebHover.tsx @@ -1,3 +1,5 @@ -export function SubtleWebHover({}: {hover: boolean}) { +import {ViewStyleProp} from '#/alf' + +export function SubtleWebHover({}: ViewStyleProp & {hover: boolean}) { return null } diff --git a/src/components/SubtleWebHover.web.tsx b/src/components/SubtleWebHover.web.tsx index e98251e0dd..adabf46bfd 100644 --- a/src/components/SubtleWebHover.web.tsx +++ b/src/components/SubtleWebHover.web.tsx @@ -2,9 +2,12 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {isTouchDevice} from '#/lib/browser' -import {useTheme} from '#/alf' +import {useTheme, ViewStyleProp} from '#/alf' -export function SubtleWebHover({hover}: {hover: boolean}) { +export function SubtleWebHover({ + style, + hover, +}: ViewStyleProp & {hover: boolean}) { const t = useTheme() if (isTouchDevice) { return null @@ -26,9 +29,8 @@ export function SubtleWebHover({hover}: {hover: boolean}) { style={[ t.atoms.bg_contrast_25, styles.container, - { - opacity: hover ? opacity : 0, - }, + {opacity: hover ? opacity : 0}, + style, ]} /> ) diff --git a/src/components/dialogs/VerifyEmailDialog.tsx b/src/components/dialogs/VerifyEmailDialog.tsx index 8dfb9bc490..d4412b6f89 100644 --- a/src/components/dialogs/VerifyEmailDialog.tsx +++ b/src/components/dialogs/VerifyEmailDialog.tsx @@ -18,8 +18,14 @@ import {Text} from '#/components/Typography' export function VerifyEmailDialog({ control, + onCloseWithoutVerifying, + onCloseAfterVerifying, + reasonText, }: { control: Dialog.DialogControlProps + onCloseWithoutVerifying?: () => void + onCloseAfterVerifying?: () => void + reasonText?: string }) { const agent = useAgent() @@ -30,18 +36,24 @@ export function VerifyEmailDialog({ control={control} onClose={async () => { if (!didVerify) { + onCloseWithoutVerifying?.() return } try { await agent.resumeSession(agent.session!) + onCloseAfterVerifying?.() } catch (e: unknown) { logger.error(String(e)) return } }}> - + ) } @@ -49,9 +61,11 @@ export function VerifyEmailDialog({ export function Inner({ control, setDidVerify, + reasonText, }: { control: Dialog.DialogControlProps setDidVerify: (value: boolean) => void + reasonText?: string }) { const {_} = useLingui() const {currentAccount} = useSession() @@ -135,26 +149,32 @@ export function Inner({ {currentStep === 'StepOne' ? ( <> - - You'll receive an email at{' '} - - {currentAccount?.email} - {' '} - to verify it's you. - {' '} - { - e.preventDefault() - control.close(() => { - openModal({name: 'change-email'}) - }) - return false - }}> - Need to change it? - + {!reasonText ? ( + <> + + You'll receive an email at{' '} + + {currentAccount?.email} + {' '} + to verify it's you. + {' '} + { + e.preventDefault() + control.close(() => { + openModal({name: 'change-email'}) + }) + return false + }}> + Need to change it? + + + ) : ( + reasonText + )} ) : ( uiStrings[currentStep].message diff --git a/src/components/dms/MessageProfileButton.tsx b/src/components/dms/MessageProfileButton.tsx index 932982d05e..22936b4c06 100644 --- a/src/components/dms/MessageProfileButton.tsx +++ b/src/components/dms/MessageProfileButton.tsx @@ -3,14 +3,18 @@ import {View} from 'react-native' import {AppBskyActorDefs} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' +import {useEmail} from '#/lib/hooks/useEmail' +import {NavigationProp} from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members' import {atoms as a, useTheme} from '#/alf' -import {ButtonIcon} from '#/components/Button' +import {Button, ButtonIcon} from '#/components/Button' import {canBeMessaged} from '#/components/dms/util' import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message' -import {Link} from '#/components/Link' +import {useDialogControl} from '../Dialog' +import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog' export function MessageProfileButton({ profile, @@ -19,15 +23,29 @@ export function MessageProfileButton({ }) { const {_} = useLingui() const t = useTheme() + const navigation = useNavigation() + const {needsEmailVerification} = useEmail() + const verifyEmailControl = useDialogControl() const {data: convo, isPending} = useMaybeConvoForUser(profile.did) const onPress = React.useCallback(() => { + if (!convo?.id) { + return + } + + if (needsEmailVerification) { + verifyEmailControl.open() + return + } + if (convo && !convo.lastMessage) { logEvent('chat:create', {logContext: 'ProfileHeader'}) } logEvent('chat:open', {logContext: 'ProfileHeader'}) - }, [convo]) + + navigation.navigate('MessagesConversation', {conversation: convo.id}) + }, [needsEmailVerification, verifyEmailControl, convo, navigation]) if (isPending) { // show pending state based on declaration @@ -53,18 +71,26 @@ export function MessageProfileButton({ if (convo) { return ( - - - + <> + + + ) } else { return null diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index ab9ec16e4d..6c3bbf2161 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -147,7 +147,7 @@ function HeaderReady({ const isDeletedAccount = profile?.handle === 'missing.invalid' const displayName = isDeletedAccount - ? 'Deleted Account' + ? _(msg`Deleted Account`) : sanitizeDisplayName( profile.displayName || profile.handle, moderation.ui('displayName'), diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx index e80fef2d7e..f402201a21 100644 --- a/src/components/dms/dialogs/NewChatDialog.tsx +++ b/src/components/dms/dialogs/NewChatDialog.tsx @@ -2,6 +2,7 @@ import React, {useCallback} from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useEmail} from '#/lib/hooks/useEmail' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' @@ -9,6 +10,8 @@ import {FAB} from '#/view/com/util/fab/FAB' import * as Toast from '#/view/com/util/Toast' import {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' +import {useDialogControl} from '#/components/Dialog' +import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {SearchablePeopleList} from './SearchablePeopleList' @@ -21,6 +24,8 @@ export function NewChat({ }) { const t = useTheme() const {_} = useLingui() + const {needsEmailVerification} = useEmail() + const verifyEmailControl = useDialogControl() const {mutate: createChat} = useGetConvoForMembers({ onSuccess: data => { @@ -48,7 +53,13 @@ export function NewChat({ <> { + if (needsEmailVerification) { + verifyEmailControl.open() + } else { + control.open() + } + }} icon={} accessibilityRole="button" accessibilityLabel={_(msg`New chat`)} @@ -62,6 +73,13 @@ export function NewChat({ onSelectChat={onCreateChat} /> + + ) } diff --git a/src/components/icons/CalendarClock.tsx b/src/components/icons/CalendarClock.tsx new file mode 100644 index 0000000000..52ba8094e0 --- /dev/null +++ b/src/components/icons/CalendarClock.tsx @@ -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', +}) diff --git a/src/components/moderation/LabelPreference.tsx b/src/components/moderation/LabelPreference.tsx index ecdbcfd258..e6f18f1d6f 100644 --- a/src/components/moderation/LabelPreference.tsx +++ b/src/components/moderation/LabelPreference.tsx @@ -22,7 +22,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) { + {showConfig && ( - + <> {cantConfigure ? ( )} - + )} ) diff --git a/src/lib/hooks/useEmail.ts b/src/lib/hooks/useEmail.ts new file mode 100644 index 0000000000..6e52846d12 --- /dev/null +++ b/src/lib/hooks/useEmail.ts @@ -0,0 +1,19 @@ +import {useServiceConfigQuery} from '#/state/queries/email-verification-required' +import {useSession} from '#/state/session' +import {BSKY_SERVICE} from '../constants' +import {getHostnameFromUrl} from '../strings/url-helpers' + +export function useEmail() { + const {currentAccount} = useSession() + + const {data: serviceConfig} = useServiceConfigQuery() + + const isSelfHost = + serviceConfig?.checkEmailConfirmed && + currentAccount && + getHostnameFromUrl(currentAccount.service) !== + getHostnameFromUrl(BSKY_SERVICE) + const needsEmailVerification = !isSelfHost && !currentAccount?.emailConfirmed + + return {needsEmailVerification} +} diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index e2e646a3d3..ee09adaf0e 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -4,10 +4,11 @@ import {useKeyboardController} from 'react-native-keyboard-controller' import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useFocusEffect} from '@react-navigation/native' +import {useFocusEffect, useNavigation} from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {CommonNavigatorParams} from '#/lib/routes/types' +import {useEmail} from '#/lib/hooks/useEmail' +import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {isWeb} from '#/platform/detection' import {useProfileShadow} from '#/state/cache/profile-shadow' import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' @@ -19,6 +20,8 @@ import {useSetMinimalShellMode} from '#/state/shell' import {CenteredView} from '#/view/com/util/Views' import {MessagesList} from '#/screens/Messages/components/MessagesList' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {useDialogControl} from '#/components/Dialog' +import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter' import {MessagesListHeader} from '#/components/dms/MessagesListHeader' import {Error} from '#/components/Error' @@ -161,8 +164,12 @@ function InnerReady({ hasScrolled: boolean setHasScrolled: React.Dispatch> }) { + const {_} = useLingui() const convoState = useConvo() + const navigation = useNavigation() const recipient = useProfileShadow(recipientUnshadowed) + const verifyEmailControl = useDialogControl() + const {needsEmailVerification} = useEmail() const moderation = React.useMemo(() => { return moderateProfile(recipient, moderationOpts) @@ -179,6 +186,12 @@ function InnerReady({ } }, [moderation]) + React.useEffect(() => { + if (needsEmailVerification) { + verifyEmailControl.open() + } + }, [needsEmailVerification, verifyEmailControl]) + return ( <> )} + { + navigation.navigate('Home') + }} + /> ) } diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index bb9c1cd4cb..6b8deea30e 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -104,7 +104,7 @@ function ChatListItemReady({ const isDeletedAccount = profile.handle === 'missing.invalid' const displayName = isDeletedAccount - ? 'Deleted Account' + ? _(msg`Deleted Account`) : sanitizeDisplayName( profile.displayName || profile.handle, moderation.ui('displayName'), diff --git a/src/screens/Messages/components/MessageInput.tsx b/src/screens/Messages/components/MessageInput.tsx index 21d6e574ea..8edad6272e 100644 --- a/src/screens/Messages/components/MessageInput.tsx +++ b/src/screens/Messages/components/MessageInput.tsx @@ -18,6 +18,7 @@ import Graphemer from 'graphemer' import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' +import {useEmail} from '#/lib/hooks/useEmail' import {isIOS} from '#/platform/detection' import { useMessageDraft, @@ -61,10 +62,15 @@ export function MessageInput({ const [message, setMessage] = React.useState(getDraft) const inputRef = useAnimatedRef() + const {needsEmailVerification} = useEmail() + useSaveMessageDraft(message) useExtractEmbedFromFacets(message, setEmbed) const onSubmit = React.useCallback(() => { + if (needsEmailVerification) { + return + } if (!hasEmbed && message.trim() === '') { return } @@ -84,6 +90,7 @@ export function MessageInput({ inputRef.current?.focus() }, 100) }, [ + needsEmailVerification, hasEmbed, message, clearDraft, @@ -159,6 +166,7 @@ export function MessageInput({ ref={inputRef} hitSlop={HITSLOP_10} animatedProps={animatedProps} + editable={!needsEmailVerification} /> + onPress={onSubmit} + disabled={needsEmailVerification}> diff --git a/src/state/queries/email-verification-required.ts b/src/state/queries/email-verification-required.ts new file mode 100644 index 0000000000..94ff5cbc68 --- /dev/null +++ b/src/state/queries/email-verification-required.ts @@ -0,0 +1,25 @@ +import {useQuery} from '@tanstack/react-query' + +interface ServiceConfig { + checkEmailConfirmed: boolean +} + +export function useServiceConfigQuery() { + return useQuery({ + queryKey: ['service-config'], + queryFn: async () => { + const res = await fetch( + 'https://api.bsky.app/xrpc/app.bsky.unspecced.getConfig', + ) + if (!res.ok) { + return { + checkEmailConfirmed: false, + } + } + + const json = await res.json() + return json as ServiceConfig + }, + staleTime: 5 * 60 * 1000, + }) +} diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 1899966dcb..a581cb79e2 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -58,6 +58,7 @@ import {EmbeddingDisabledError} from '#/lib/api/resolve' import {until} from '#/lib/async/until' import {MAX_GRAPHEME_LENGTH} from '#/lib/constants' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' +import {useEmail} from '#/lib/hooks/useEmail' import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {usePalette} from '#/lib/hooks/usePalette' @@ -110,6 +111,8 @@ import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, native, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' @@ -297,6 +300,15 @@ export const ComposePost = ({ } }, [onPressCancel, closeAllDialogs, closeAllModals]) + const {needsEmailVerification} = useEmail() + const emailVerificationControl = useDialogControl() + + useEffect(() => { + if (needsEmailVerification) { + emailVerificationControl.open() + } + }, [needsEmailVerification, emailVerificationControl]) + const missingAltError = useMemo(() => { if (!requireAltTextEnabled) { return @@ -570,6 +582,15 @@ export const ComposePost = ({ const isWebFooterSticky = !isNative && thread.posts.length > 1 return ( + { + onClose() + }} + reasonText={_( + msg`Before creating a post, you must first verify your email.`, + )} + /> - Porn + Adult - - {labels.includes('sexual') ? ( - Pictures meant for adults. - ) : labels.includes('nudity') ? ( - Artistic or non-erotic nudity. - ) : labels.includes('porn') ? ( - Sexual activity or erotic nudity. - ) : ( - Does not contain adult content. - )} - + {labels.includes('sexual') || + labels.includes('nudity') || + labels.includes('porn') ? ( + + {labels.includes('sexual') ? ( + Pictures meant for adults. + ) : labels.includes('nudity') ? ( + Artistic or non-erotic nudity. + ) : labels.includes('porn') ? ( + Sexual activity or erotic nudity. + ) : ( + '' + )} + + ) : null} @@ -203,16 +207,14 @@ function DialogInner({ - - {labels.includes('graphic-media') ? ( + {labels.includes('graphic-media') ? ( + Media that may be disturbing or inappropriate for some audiences. - ) : ( - Does not contain graphic or disturbing content. - )} - + + ) : null} diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 3276cf8821..707aad7fb0 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -162,7 +162,10 @@ export function FeedSourceCardLoaded({ style={[ pal.border, { - borderTopWidth: showMinimalPlaceholder || hideTopBorder ? 0 : 1, + borderTopWidth: + showMinimalPlaceholder || hideTopBorder + ? 0 + : StyleSheet.hairlineWidth, flexDirection: 'row', alignItems: 'center', flex: 1, diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index ab8306b36a..e9bc1523d1 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -62,7 +62,7 @@ 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 SLOW_SPRING = {stiffness: isIOS ? 180 : 250} const FAST_SPRING = {stiffness: 700} export default function ImageViewRoot({ @@ -433,7 +433,7 @@ function LightboxImage({ if (openProgress.value !== 1 || isFlyingAway.value) { return } - if (Math.abs(e.velocityY) > 1000) { + if (Math.abs(e.velocityY) > 200) { isFlyingAway.value = true if (dismissSwipeTranslateY.value === 0) { // HACK: If the initial value is 0, withDecay() animation doesn't start. @@ -442,7 +442,7 @@ function LightboxImage({ } dismissSwipeTranslateY.value = withDecay({ velocity: e.velocityY, - velocityFactor: Math.max(3000 / Math.abs(e.velocityY), 1), // Speed up if it's too slow. + 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 { diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 5473fff854..b90f2ecd64 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -467,7 +467,12 @@ let FeedItem = ({ {item.type === 'feedgen-like' && item.subjectUri ? ( ) : null} @@ -778,7 +783,6 @@ const styles = StyleSheet.create({ opacity: 0.8, }, feedcard: { - borderWidth: 1, borderRadius: 8, paddingVertical: 12, marginTop: 6, diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 5044f9621a..e8fdc47f79 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -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 ( - - + + This post has been deleted. @@ -308,7 +314,7 @@ let PostThreadItemLoaded = ({ /> - @@ -317,10 +323,10 @@ let PostThreadItemLoaded = ({ sanitizeHandle(post.author.handle), moderation.ui('displayName'), )} - + - {sanitizeHandle(post.author.handle, '@')} - + {currentAccount?.did !== post.author.did && ( @@ -393,48 +399,48 @@ let PostThreadItemLoaded = ({ ]}> {post.repostCount != null && post.repostCount !== 0 ? ( - - + {formatCount(i18n, post.repostCount)} - {' '} + {' '} - + ) : null} {post.quoteCount != null && post.quoteCount !== 0 && !post.viewer?.embeddingDisabled ? ( - - + {formatCount(i18n, post.quoteCount)} - {' '} + {' '} - + ) : null} {post.likeCount != null && post.likeCount !== 0 ? ( - - + {formatCount(i18n, post.likeCount)} - {' '} + {' '} - + ) : null} @@ -617,13 +623,13 @@ let PostThreadItemLoaded = ({ href={postHref} title={itemTitle} noFeedback> - + More - ) : 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 ( { - setHover(true) - }} - onPointerLeave={() => { - setHover(false) - }}> + onPointerEnter={onHoverIn} + onPointerLeave={onHoverOut}> {Array.from(Array(depth - 1)).map((_, n: number) => ( ))} - {children} + + + {children} + ) } return ( { - setHover(true) - }} - onPointerLeave={() => { - setHover(false) - }} + onPointerEnter={onHoverIn} + onPointerLeave={onHoverOut} style={[ a.border_t, a.px_sm, @@ -732,32 +741,124 @@ function ExpandedPostDetails({ }, [openLink, translatorUrl]) return ( - - - {niceDate(i18n, post.indexedAt)} - - {isRootPost && ( - - )} - {needsTranslation && ( - <> - - · - + + + + + {niceDate(i18n, post.indexedAt)} + + {isRootPost && ( + + )} + {needsTranslation && ( + <> + + · + - - Translate - - - )} + + Translate + + + )} + ) } +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 ( + <> + + + + + Archived post + + + + This post claims to have been created on{' '} + {niceDate(i18n, createdAt)}, + but was first seen by Bluesky on{' '} + {niceDate(i18n, indexedAt)}. + + + + + Bluesky cannot confirm the authenticity of the claimed date. + + + + {}} /> + + + + ) +} + function getThreadAuthor( post: AppBskyFeedDefs.PostView, record: AppBskyFeedPost.Record, diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx index dc68ee7a17..09335fa0ed 100644 --- a/src/view/com/posts/FeedSlice.tsx +++ b/src/view/com/posts/FeedSlice.tsx @@ -7,6 +7,8 @@ import {Trans} from '@lingui/macro' import {usePalette} from '#/lib/hooks/usePalette' import {makeProfileLink} from '#/lib/routes/links' import {FeedPostSlice} from '#/state/queries/post-feed' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {SubtleWebHover} from '#/components/SubtleWebHover' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {FeedItem} from './FeedItem' @@ -108,6 +110,11 @@ FeedSlice = memo(FeedSlice) export {FeedSlice} function ViewFullThread({uri}: {uri: string}) { + const { + state: hover, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() const pal = usePalette('default') const itemHref = React.useMemo(() => { const urip = new AtUri(uri) @@ -115,7 +122,18 @@ function ViewFullThread({uri}: {uri: string}) { }, [uri]) return ( - + + ( const isScrolledDown = useSharedValue(false) const t = useTheme() const dedupe = useDedupe(400) + const {activeLightbox} = useLightbox() function handleScrolledDownChange(didScrollDown: boolean) { onScrolledDownChange?.(didScrollDown) @@ -143,6 +145,7 @@ function ListImpl( contentOffset={contentOffset} refreshControl={refreshControl} onScroll={scrollHandler} + scrollsToTop={!activeLightbox} scrollEventThrottle={1} onViewableItemsChanged={onViewableItemsChanged} viewabilityConfig={viewabilityConfig} diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 5ddc4ea8a2..d9a2e351e1 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -468,7 +468,7 @@ let Row = function RowImpl({ } } else { if (intersectionTimeout.current) { - clearTimeout(intersectionTimeout.current) + clearTimeout(intersectionTimeout.current as NodeJS.Timeout) intersectionTimeout.current = undefined } } diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx index 23dffc561d..193d07d724 100644 --- a/src/view/com/util/MainScrollProvider.tsx +++ b/src/view/com/util/MainScrollProvider.tsx @@ -3,6 +3,7 @@ import {NativeScrollEvent} from 'react-native' import { cancelAnimation, interpolate, + makeMutable, useSharedValue, withSpring, } from 'react-native-reanimated' @@ -20,6 +21,18 @@ function clamp(num: number, min: number, max: number) { return Math.min(Math.max(num, min), max) } +const V0 = makeMutable( + withSpring(0, { + overshootClamping: true, + }), +) + +const V1 = makeMutable( + withSpring(1, { + overshootClamping: true, + }), +) + export function MainScrollProvider({children}: {children: React.ReactNode}) { const {headerHeight} = useShellLayout() const {headerMode} = useMinimalShellMode() @@ -31,9 +44,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { (v: boolean) => { 'worklet' cancelAnimation(headerMode) - headerMode.value = withSpring(v ? 1 : 0, { - overshootClamping: true, - }) + headerMode.value = v ? V1.value : V0.value }, [headerMode], ) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index c0166a16ee..5384f6827e 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -49,6 +49,8 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { precacheProfile(queryClient, opts.author) }, [queryClient, opts.author]) + const timestampLabel = niceDate(i18n, opts.timestamp) + return ( { {({timeElapsed}) => ( export function ListsScreen({}: Props) { + const {_} = useLingui() const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const {isMobile} = useWebMediaQueries() const navigation = useNavigation() const {openModal} = useModalControls() + const {needsEmailVerification} = useEmail() + const control = useDialogControl() useFocusEffect( React.useCallback(() => { @@ -33,6 +40,11 @@ export function ListsScreen({}: Props) { ) const onPressNewList = React.useCallback(() => { + if (needsEmailVerification) { + control.open() + return + } + openModal({ name: 'create-or-edit-list', purpose: 'app.bsky.graph.defs#curatelist', @@ -46,7 +58,7 @@ export function ListsScreen({}: Props) { } catch {} }, }) - }, [openModal, navigation]) + }, [needsEmailVerification, control, openModal, navigation]) return ( @@ -87,6 +99,12 @@ export function ListsScreen({}: Props) { + ) } diff --git a/src/view/screens/ModerationModlists.tsx b/src/view/screens/ModerationModlists.tsx index b147ba502d..c623c5376f 100644 --- a/src/view/screens/ModerationModlists.tsx +++ b/src/view/screens/ModerationModlists.tsx @@ -2,9 +2,11 @@ import React from 'react' import {View} from 'react-native' import {AtUri} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Trans} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useFocusEffect, useNavigation} from '@react-navigation/native' +import {useEmail} from '#/lib/hooks/useEmail' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' @@ -16,15 +18,20 @@ import {MyLists} from '#/view/com/lists/MyLists' import {Button} from '#/view/com/util/forms/Button' import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' import {Text} from '#/view/com/util/text/Text' +import {useDialogControl} from '#/components/Dialog' +import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps export function ModerationModlistsScreen({}: Props) { + const {_} = useLingui() const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const {isMobile} = useWebMediaQueries() const navigation = useNavigation() const {openModal} = useModalControls() + const {needsEmailVerification} = useEmail() + const control = useDialogControl() useFocusEffect( React.useCallback(() => { @@ -33,6 +40,11 @@ export function ModerationModlistsScreen({}: Props) { ) const onPressNewList = React.useCallback(() => { + if (needsEmailVerification) { + control.open() + return + } + openModal({ name: 'create-or-edit-list', purpose: 'app.bsky.graph.defs#modlist', @@ -46,7 +58,7 @@ export function ModerationModlistsScreen({}: Props) { } catch {} }, }) - }, [openModal, navigation]) + }, [needsEmailVerification, control, openModal, navigation]) return ( @@ -83,6 +95,12 @@ export function ModerationModlistsScreen({}: Props) { + ) } diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 257506dd0b..3dc2b076c4 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -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() @@ -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 => { - - - - Terms of Service - - - Privacy Policy - - {kawaii && ( - - - Logo by{' '} - - @sawaratsuki.bsky.social - - - - )} - + @@ -633,3 +604,39 @@ function MenuItem({icon, label, count, bold, onPress}: MenuItemProps) { ) } + +function ExtraLinks() { + const {_} = useLingui() + const t = useTheme() + const kawaii = useKawaiiMode() + + return ( + + + Terms of Service + + + Privacy Policy + + {kawaii && ( + + + Logo by{' '} + + @sawaratsuki.bsky.social + + + + )} + + ) +} diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index 9b34159d79..81855c97d0 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -177,7 +177,7 @@ export function BottomBarWeb() { alignItems: 'center', justifyContent: 'space-between', paddingTop: 14, - paddingBottom: 2, + paddingBottom: 14, paddingLeft: 14, paddingRight: 6, gap: 8, diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 84d6994b3a..f554373562 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -32,8 +32,9 @@ function ShellInner() { const navigator = useNavigation() const closeAllActiveElements = useCloseAllActiveElements() const {_} = useLingui() + const showDrawer = !isDesktop && isDrawerOpen - useWebBodyScrollLock(isDrawerOpen) + useWebBodyScrollLock(showDrawer) useComposerKeyboardShortcut() useIntentHandler() @@ -56,7 +57,7 @@ function ShellInner() { - {!isDesktop && isDrawerOpen && ( + {showDrawer && ( { // Only close if press happens outside of the drawer diff --git a/yarn.lock b/yarn.lock index c622dd1bd8..9af5cabcdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7523,10 +7523,12 @@ dependencies: undici-types "~5.26.4" -"@types/node@^18.16.2": - version "18.17.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.6.tgz#0296e9a30b22d2a8fcaa48d3c45afe51474ca55b" - integrity sha512-fGmT/P7z7ecA6bv/ia5DlaWCH4YeZvAQMNpUhrJjtAhOhZfoxS1VLUgU2pdk63efSjQaOJWdXMuAJsws+8I6dg== +"@types/node@^20.14.3": + version "20.17.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.6.tgz#6e4073230c180d3579e8c60141f99efdf5df0081" + integrity sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ== + dependencies: + undici-types "~6.19.2" "@types/parse-json@^4.0.0": version "4.0.0" @@ -18321,12 +18323,12 @@ react-native-dotenv@^3.3.1: dependencies: dotenv "^16.3.1" -react-native-drawer-layout@^4.0.0-alpha.3: - version "4.0.0-alpha.3" - resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-4.0.0-alpha.3.tgz#0adf51e816f2ce094d415fe33d9f43c1e6df6ae2" - integrity sha512-BRlX8l2gDD41fs8RfaemjDBDV0PPspDMvej/3b9QWfp+JQ/H8tkSdWBtUBbSBzYdqyqy2bHV4epOCMb4t+0B0g== +react-native-drawer-layout@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-4.0.1.tgz#5f2b4bae43b9be6c6b92544b63c5b4767b279933" + integrity sha512-395LypsQKNqcaMFHDj41jPXe3m9LbQRF7XKR1BzJh8B/kkc95OTZhwpXzMRBOLUiq4y4yL0loRzQtQRiruG+fQ== dependencies: - use-latest-callback "^0.1.9" + use-latest-callback "^0.2.1" react-native-gesture-handler@2.20.0: version "2.20.0" @@ -19968,16 +19970,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -20086,7 +20079,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -20100,13 +20093,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -20923,6 +20909,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + undici@^5.28.2: version "5.28.2" resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.2.tgz#fea200eac65fc7ecaff80a023d1a0543423b4c91" @@ -21119,6 +21110,11 @@ use-latest-callback@^0.1.9: resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.9.tgz#10191dc54257e65a8e52322127643a8940271e2a" integrity sha512-CL/29uS74AwreI/f2oz2hLTW7ZqVeV5+gxFeGudzQrgkCytrHw33G4KbnQOrRlAEzzAFXi7dDLMC9zhWcVpzmw== +use-latest-callback@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.1.tgz#4d4e6a9e4817b13142834850dcfa8d24ca4569cf" + integrity sha512-QWlq8Is8BGWBf883QOEQP5HWYX/kMI+JTbJ5rdtvJLmXTIh9XoHIO3PQcmQl8BU44VKxow1kbQUHa6mQSMALDQ== + use-latest@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.1.tgz#d13dfb4b08c28e3e33991546a2cee53e14038cf2" @@ -21835,7 +21831,7 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -21853,15 +21849,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"