diff --git a/app.config.js b/app.config.js index 82d0d5a0ca..1f4de0370b 100644 --- a/app.config.js +++ b/app.config.js @@ -6,7 +6,7 @@ module.exports = function () { slug: 'bluesky', scheme: 'bluesky', owner: 'blueskysocial', - version: '1.52.0', + version: '1.53.0', runtimeVersion: { policy: 'appVersion', }, @@ -19,7 +19,7 @@ module.exports = function () { backgroundColor: '#ffffff', }, ios: { - buildNumber: '2', + buildNumber: '1', supportsTablet: false, bundleIdentifier: 'xyz.blueskyweb.app', config: { @@ -43,7 +43,7 @@ module.exports = function () { backgroundColor: '#ffffff', }, android: { - versionCode: 41, + versionCode: 42, adaptiveIcon: { foregroundImage: './assets/adaptive-icon.png', backgroundColor: '#ffffff', diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index d5d864069b..5be96ce0e3 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -91,6 +91,11 @@ func serve(cctx *cli.Context) error { } e.HideBanner = true + e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug) + e.HTTPErrorHandler = server.errorHandler + + e.IPExtractor = echo.ExtractIPFromXFFHeader() + // SECURITY: Do not modify without due consideration. e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ ContentTypeNosniff: "nosniff", @@ -106,8 +111,23 @@ func serve(cctx *cli.Context) error { return strings.HasPrefix(c.Request().URL.Path, "/static") }, })) - e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug) - e.HTTPErrorHandler = server.errorHandler + e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ + Skipper: middleware.DefaultSkipper, + Store: middleware.NewRateLimiterMemoryStoreWithConfig( + middleware.RateLimiterMemoryStoreConfig{ + Rate: 10, // requests per second + Burst: 30, // allow bursts + ExpiresIn: 3 * time.Minute, // garbage collect entries older than 3 minutes + }, + ), + IdentifierExtractor: func(ctx echo.Context) (string, error) { + id := ctx.RealIP() + return id, nil + }, + DenyHandler: func(c echo.Context, identifier string, err error) error { + return c.String(http.StatusTooManyRequests, "Your request has been rate limited. Please try again later. Contact security@bsky.app if you believe this was a mistake.\n") + }, + })) // redirect trailing slash to non-trailing slash. // all of our current endpoints have no trailing slash. diff --git a/package.json b/package.json index de2e861da5..fdb6480f92 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.52.0", + "version": "1.53.0", "private": true, "scripts": { "prepare": "is-ci || husky install", @@ -217,7 +217,7 @@ }, "resolutions": { "@types/react": "^18", - "**/zeed-dom": "estrattonbailey/zeed-dom#publish" + "**/zeed-dom": "0.10.9" }, "jest": { "preset": "jest-expo/ios", diff --git a/src/lib/hooks/useMinimalShellMode.tsx b/src/lib/hooks/useMinimalShellMode.tsx index e28a0e884f..68f405dc4d 100644 --- a/src/lib/hooks/useMinimalShellMode.tsx +++ b/src/lib/hooks/useMinimalShellMode.tsx @@ -1,4 +1,5 @@ import React from 'react' +import {autorun} from 'mobx' import {useStores} from 'state/index' import {Animated} from 'react-native' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' @@ -7,26 +8,29 @@ export function useMinimalShellMode() { const store = useStores() const minimalShellInterp = useAnimatedValue(0) const footerMinimalShellTransform = { - transform: [{translateY: Animated.multiply(minimalShellInterp, 100)}], + opacity: Animated.subtract(1, minimalShellInterp), + transform: [{translateY: Animated.multiply(minimalShellInterp, 50)}], } React.useEffect(() => { - if (store.shell.minimalShellMode) { - Animated.timing(minimalShellInterp, { - toValue: 1, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - } else { - Animated.timing(minimalShellInterp, { - toValue: 0, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - } - }, [minimalShellInterp, store.shell.minimalShellMode]) + return autorun(() => { + if (store.shell.minimalShellMode) { + Animated.timing(minimalShellInterp, { + toValue: 1, + duration: 150, + useNativeDriver: true, + isInteraction: false, + }).start() + } else { + Animated.timing(minimalShellInterp, { + toValue: 0, + duration: 150, + useNativeDriver: true, + isInteraction: false, + }).start() + } + }) + }, [minimalShellInterp, store]) return {footerMinimalShellTransform} } diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index e39e2dd68d..7924666e5c 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -1,6 +1,7 @@ import React, {useMemo} from 'react' import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' +import {autorun} from 'mobx' import {TabBar} from 'view/com/pager/TabBar' import {RenderTabBarFnProps} from 'view/com/pager/Pager' import {useStores} from 'state/index' @@ -22,15 +23,18 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( const interp = useAnimatedValue(0) React.useEffect(() => { - Animated.timing(interp, { - toValue: store.shell.minimalShellMode ? 1 : 0, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - }, [interp, store.shell.minimalShellMode]) + return autorun(() => { + Animated.timing(interp, { + toValue: store.shell.minimalShellMode ? 1 : 0, + duration: 150, + useNativeDriver: true, + isInteraction: false, + }).start() + }) + }, [interp, store]) const transform = { - transform: [{translateY: Animated.multiply(interp, -100)}], + opacity: Animated.subtract(1, interp), + transform: [{translateY: Animated.multiply(interp, -50)}], } const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3) @@ -45,7 +49,14 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( ) return ( - + { - if (store.shell.minimalShellMode) { - Animated.timing(interp, { - toValue: 1, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - } else { - Animated.timing(interp, { - toValue: 0, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - } - }, [interp, store.shell.minimalShellMode]) + return autorun(() => { + if (store.shell.minimalShellMode) { + Animated.timing(interp, { + toValue: 1, + duration: 100, + useNativeDriver: true, + isInteraction: false, + }).start() + } else { + Animated.timing(interp, { + toValue: 0, + duration: 100, + useNativeDriver: true, + isInteraction: false, + }).start() + } + }) + }, [interp, store]) const transform = { transform: [{translateY: Animated.multiply(interp, -100)}], } diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index 6c0e4c6cc4..935d93033b 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -144,8 +144,6 @@ export function Selector({ items: string[] onSelect?: (index: number) => void }) { - const [height, setHeight] = useState(0) - const pal = usePalette('default') const borderColor = useColorSchemeStyle( {borderColor: colors.black}, @@ -160,22 +158,13 @@ export function Selector({ - { - const {height: layoutHeight} = e.nativeEvent.layout - setHeight(layoutHeight || 60) - }}> + showsHorizontalScrollIndicator={false}> + {items.map((item, i) => { const selected = i === selectedIndex return ( diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index 6c96eef2c1..97eeba358a 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -1,5 +1,6 @@ import React, {ComponentProps} from 'react' import {observer} from 'mobx-react-lite' +import {autorun} from 'mobx' import {Animated, StyleSheet, TouchableWithoutFeedback} from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {gradients} from 'lib/styles' @@ -25,13 +26,15 @@ export const FABInner = observer(function FABInnerImpl({ const store = useStores() const interp = useAnimatedValue(0) React.useEffect(() => { - Animated.timing(interp, { - toValue: store.shell.minimalShellMode ? 0 : 1, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - }, [interp, store.shell.minimalShellMode]) + return autorun(() => { + Animated.timing(interp, { + toValue: store.shell.minimalShellMode ? 0 : 1, + duration: 100, + useNativeDriver: true, + isInteraction: false, + }).start() + }) + }, [interp, store]) const transform = isTablet ? undefined : { diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 679f71c99b..094b0c56cc 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -23,19 +23,19 @@ export const GalleryItem: FC = ({ onLongPress, }) => { const image = images[index] - return ( - + onPress(index) : undefined} onPressIn={onPressIn ? () => onPressIn(index) : undefined} onLongPress={onLongPress ? () => onLongPress(index) : undefined} + style={styles.fullWidth} accessibilityRole="button" accessibilityLabel={image.alt || 'Image'} accessibilityHint=""> = ({ } const styles = StyleSheet.create({ + fullWidth: { + flex: 1, + }, + image: { + flex: 1, + borderRadius: 4, + }, altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', borderRadius: 6, paddingHorizontal: 6, paddingVertical: 3, position: 'absolute', - left: 6, - bottom: 6, + left: 8, + bottom: 8, }, alt: { color: 'white', diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index 4c09013043..2e352d0864 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -1,13 +1,5 @@ -import React, {useMemo, useState} from 'react' -import { - LayoutChangeEvent, - StyleProp, - StyleSheet, - View, - ViewStyle, -} from 'react-native' -import {ImageStyle} from 'expo-image' -import {Dimensions} from 'lib/media/types' +import React from 'react' +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {AppBskyEmbedImages} from '@atproto/api' import {GalleryItem} from './Gallery' @@ -20,21 +12,11 @@ interface ImageLayoutGridProps { } export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) { - const [containerInfo, setContainerInfo] = useState() - - const onLayout = (evt: LayoutChangeEvent) => { - const {width, height} = evt.nativeEvent.layout - setContainerInfo({ - width, - height, - }) - } - return ( - - {containerInfo ? ( - - ) : undefined} + + + + ) } @@ -44,70 +26,80 @@ interface ImageLayoutGridInnerProps { onPress?: (index: number) => void onLongPress?: (index: number) => void onPressIn?: (index: number) => void - containerInfo: Dimensions } -function ImageLayoutGridInner({ - containerInfo, - ...props -}: ImageLayoutGridInnerProps) { +function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { const count = props.images.length - const size1 = useMemo(() => { - if (count === 3) { - const size = (containerInfo.width - 10) / 3 - return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} - } else { - const size = (containerInfo.width - 5) / 2 - return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} - } - }, [count, containerInfo]) - const size2 = React.useMemo(() => { - if (count === 3) { - const size = ((containerInfo.width - 10) / 3) * 2 + 5 - return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} - } else { - const size = (containerInfo.width - 5) / 2 - return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} - } - }, [count, containerInfo]) switch (count) { case 2: return ( - - + + + + + + ) + case 3: return ( - - - - + + + + + + + + + + ) + case 4: return ( - - - + + + + + + + - - - + + + + + + + ) + default: return null } } +// This is used to compute margins (rather than flexbox gap) due to Yoga bugs: +// https://github.com/facebook/yoga/issues/1418 +const IMAGE_GAP = 5 + const styles = StyleSheet.create({ - flexRow: {flexDirection: 'row', gap: 5}, - flexColumn: {flexDirection: 'column', gap: 5}, + container: { + marginHorizontal: -IMAGE_GAP / 2, + marginVertical: -IMAGE_GAP / 2, + }, + flexRow: {flexDirection: 'row'}, + smallItem: {flex: 1, aspectRatio: 1}, + image: { + margin: IMAGE_GAP / 2, + }, }) diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index f5d12ce2c1..57c3baa5b2 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -10,6 +10,10 @@ import {colors} from 'lib/styles' import {HITSLOP_20} from 'lib/constants' import {isWeb} from 'platform/detection' import {clamp} from 'lib/numbers' +import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated' + +const AnimatedTouchableOpacity = + Animated.createAnimatedComponent(TouchableOpacity) export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ onPress, @@ -30,15 +34,18 @@ export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ ? 50 : (minMode || isDesktop ? 16 : 60) + (isWeb ? 20 : clamp(safeAreaInsets.bottom, 15, 60)) + const animatedStyle = useAnimatedStyle(() => ({ + bottom: withTiming(bottom, {duration: 150}), + })) return ( - {showIndicator && } - + ) }) @@ -67,14 +74,12 @@ const styles = StyleSheet.create({ loadLatestTablet: { // @ts-ignore web only left: '50vw', - // @ts-ignore web only -prf - transform: 'translateX(-282px)', + transform: [{translateX: -282}], }, loadLatestDesktop: { // @ts-ignore web only left: '50vw', - // @ts-ignore web only -prf - transform: 'translateX(-382px)', + transform: [{translateX: -382}], }, indicator: { position: 'absolute', diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 4758c5e010..984aef25d9 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -87,6 +87,7 @@ export const BottomBar = observer(function BottomBarImpl({ pal.border, {paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)}, footerMinimalShellTransform, + store.shell.minimalShellMode && styles.disabled, ]}>