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 (
-
+
+
+
)
}
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"