(
retries: number,
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 9bf1fb35ea..1a13304fa9 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -21,7 +21,7 @@ export const STARTER_PACK_MAX_SIZE = 150
// code and update this number with each release until we can get the
// server route done.
// -prf
-export const JOINED_THIS_WEEK = 3060000 // estimate as of 9/6/24
+export const JOINED_THIS_WEEK = 650000 // estimate as of 10/28/24
const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
export function FEEDBACK_FORM_URL({
@@ -50,7 +50,7 @@ export const MAX_DM_GRAPHEME_LENGTH = 1000
// Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html
// but increasing limit per user feedback
-export const MAX_ALT_TEXT = 1000
+export const MAX_ALT_TEXT = 2000
export function IS_TEST_USER(handle?: string) {
return handle && handle?.endsWith('.test')
diff --git a/src/lib/custom-animations/CountWheel.tsx b/src/lib/custom-animations/CountWheel.tsx
index 2e435f7d38..b4ca62c6e2 100644
--- a/src/lib/custom-animations/CountWheel.tsx
+++ b/src/lib/custom-animations/CountWheel.tsx
@@ -8,10 +8,10 @@ import Animated, {
} from 'react-native-reanimated'
import {i18n} from '@lingui/core'
-import {decideShouldRoll} from 'lib/custom-animations/util'
-import {s} from 'lib/styles'
-import {formatCount} from 'view/com/util/numeric/format'
-import {Text} from 'view/com/util/text/Text'
+import {decideShouldRoll} from '#/lib/custom-animations/util'
+import {s} from '#/lib/styles'
+import {formatCount} from '#/view/com/util/numeric/format'
+import {Text} from '#/view/com/util/text/Text'
import {atoms as a, useTheme} from '#/alf'
const animationConfig = {
diff --git a/src/lib/custom-animations/CountWheel.web.tsx b/src/lib/custom-animations/CountWheel.web.tsx
index 78120b7078..980ed06e6c 100644
--- a/src/lib/custom-animations/CountWheel.web.tsx
+++ b/src/lib/custom-animations/CountWheel.web.tsx
@@ -3,10 +3,10 @@ import {View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated'
import {i18n} from '@lingui/core'
-import {decideShouldRoll} from 'lib/custom-animations/util'
-import {s} from 'lib/styles'
-import {formatCount} from 'view/com/util/numeric/format'
-import {Text} from 'view/com/util/text/Text'
+import {decideShouldRoll} from '#/lib/custom-animations/util'
+import {s} from '#/lib/styles'
+import {formatCount} from '#/view/com/util/numeric/format'
+import {Text} from '#/view/com/util/text/Text'
import {atoms as a, useTheme} from '#/alf'
const animationConfig = {
diff --git a/src/lib/custom-animations/GestureActionView.tsx b/src/lib/custom-animations/GestureActionView.tsx
new file mode 100644
index 0000000000..79e9db8a91
--- /dev/null
+++ b/src/lib/custom-animations/GestureActionView.tsx
@@ -0,0 +1,410 @@
+import React from 'react'
+import {ColorValue, Dimensions, StyleSheet, View} from 'react-native'
+import {Gesture, GestureDetector} from 'react-native-gesture-handler'
+import Animated, {
+ clamp,
+ interpolate,
+ interpolateColor,
+ runOnJS,
+ useAnimatedReaction,
+ useAnimatedStyle,
+ useDerivedValue,
+ useReducedMotion,
+ useSharedValue,
+ withSequence,
+ withTiming,
+} from 'react-native-reanimated'
+
+import {useHaptics} from '#/lib/haptics'
+
+interface GestureAction {
+ color: ColorValue
+ action: () => void
+ threshold: number
+ icon: React.ElementType
+}
+
+interface GestureActions {
+ leftFirst?: GestureAction
+ leftSecond?: GestureAction
+ rightFirst?: GestureAction
+ rightSecond?: GestureAction
+}
+
+const MAX_WIDTH = Dimensions.get('screen').width
+const ICON_SIZE = 32
+
+export function GestureActionView({
+ children,
+ actions,
+}: {
+ children: React.ReactNode
+ actions: GestureActions
+}) {
+ if (
+ (actions.leftSecond && !actions.leftFirst) ||
+ (actions.rightSecond && !actions.rightFirst)
+ ) {
+ throw new Error(
+ 'You must provide the first action before the second action',
+ )
+ }
+
+ const [activeAction, setActiveAction] = React.useState<
+ 'leftFirst' | 'leftSecond' | 'rightFirst' | 'rightSecond' | null
+ >(null)
+
+ const haptic = useHaptics()
+ const isReducedMotion = useReducedMotion()
+
+ const transX = useSharedValue(0)
+ const clampedTransX = useDerivedValue(() => {
+ const min = actions.leftFirst ? -MAX_WIDTH : 0
+ const max = actions.rightFirst ? MAX_WIDTH : 0
+ return clamp(transX.value, min, max)
+ })
+
+ const iconScale = useSharedValue(1)
+ const isActive = useSharedValue(false)
+ const hitFirst = useSharedValue(false)
+ const hitSecond = useSharedValue(false)
+
+ const runPopAnimation = () => {
+ 'worklet'
+ if (isReducedMotion) {
+ return
+ }
+
+ iconScale.value = withSequence(
+ withTiming(1.2, {duration: 175}),
+ withTiming(1, {duration: 100}),
+ )
+ }
+
+ useAnimatedReaction(
+ () => transX,
+ () => {
+ if (transX.value === 0) {
+ runOnJS(setActiveAction)(null)
+ } else if (transX.value < 0) {
+ if (
+ actions.leftSecond &&
+ transX.value <= -actions.leftSecond.threshold
+ ) {
+ if (activeAction !== 'leftSecond') {
+ runOnJS(setActiveAction)('leftSecond')
+ }
+ } else if (activeAction !== 'leftFirst') {
+ runOnJS(setActiveAction)('leftFirst')
+ }
+ } else if (transX.value > 0) {
+ if (
+ actions.rightSecond &&
+ transX.value > actions.rightSecond.threshold
+ ) {
+ if (activeAction !== 'rightSecond') {
+ runOnJS(setActiveAction)('rightSecond')
+ }
+ } else if (activeAction !== 'rightFirst') {
+ runOnJS(setActiveAction)('rightFirst')
+ }
+ }
+ },
+ )
+
+ const panGesture = Gesture.Pan()
+ .activeOffsetX([-10, 10])
+ // Absurdly high value so it doesn't interfere with the pan gestures above (i.e., scroll)
+ // reanimated doesn't offer great support for disabling y/x axes :/
+ .activeOffsetY([-200, 200])
+ .onStart(() => {
+ 'worklet'
+ isActive.value = true
+ })
+ .onChange(e => {
+ 'worklet'
+ transX.value = e.translationX
+
+ if (e.translationX < 0) {
+ // Left side
+ if (actions.leftSecond) {
+ if (
+ e.translationX <= -actions.leftSecond.threshold &&
+ !hitSecond.value
+ ) {
+ runPopAnimation()
+ runOnJS(haptic)()
+ hitSecond.value = true
+ } else if (
+ hitSecond.value &&
+ e.translationX > -actions.leftSecond.threshold
+ ) {
+ runPopAnimation()
+ hitSecond.value = false
+ }
+ }
+
+ if (!hitSecond.value && actions.leftFirst) {
+ if (
+ e.translationX <= -actions.leftFirst.threshold &&
+ !hitFirst.value
+ ) {
+ runPopAnimation()
+ runOnJS(haptic)()
+ hitFirst.value = true
+ } else if (
+ hitFirst.value &&
+ e.translationX > -actions.leftFirst.threshold
+ ) {
+ hitFirst.value = false
+ }
+ }
+ } else if (e.translationX > 0) {
+ // Right side
+ if (actions.rightSecond) {
+ if (
+ e.translationX >= actions.rightSecond.threshold &&
+ !hitSecond.value
+ ) {
+ runPopAnimation()
+ runOnJS(haptic)()
+ hitSecond.value = true
+ } else if (
+ hitSecond.value &&
+ e.translationX < actions.rightSecond.threshold
+ ) {
+ runPopAnimation()
+ hitSecond.value = false
+ }
+ }
+
+ if (!hitSecond.value && actions.rightFirst) {
+ if (
+ e.translationX >= actions.rightFirst.threshold &&
+ !hitFirst.value
+ ) {
+ runPopAnimation()
+ runOnJS(haptic)()
+ hitFirst.value = true
+ } else if (
+ hitFirst.value &&
+ e.translationX < actions.rightFirst.threshold
+ ) {
+ hitFirst.value = false
+ }
+ }
+ }
+ })
+ .onEnd(e => {
+ 'worklet'
+ if (e.translationX < 0) {
+ if (hitSecond.value && actions.leftSecond) {
+ runOnJS(actions.leftSecond.action)()
+ } else if (hitFirst.value && actions.leftFirst) {
+ runOnJS(actions.leftFirst.action)()
+ }
+ } else if (e.translationX > 0) {
+ if (hitSecond.value && actions.rightSecond) {
+ runOnJS(actions.rightSecond.action)()
+ } else if (hitSecond.value && actions.rightFirst) {
+ runOnJS(actions.rightFirst.action)()
+ }
+ }
+ transX.value = withTiming(0, {duration: 200})
+ hitFirst.value = false
+ hitSecond.value = false
+ isActive.value = false
+ })
+
+ const composedGesture = Gesture.Simultaneous(panGesture)
+
+ const animatedSliderStyle = useAnimatedStyle(() => {
+ return {
+ transform: [{translateX: clampedTransX.value}],
+ }
+ })
+
+ const leftSideInterpolation = React.useMemo(() => {
+ return createInterpolation({
+ firstColor: actions.leftFirst?.color,
+ secondColor: actions.leftSecond?.color,
+ firstThreshold: actions.leftFirst?.threshold,
+ secondThreshold: actions.leftSecond?.threshold,
+ side: 'left',
+ })
+ }, [actions.leftFirst, actions.leftSecond])
+
+ const rightSideInterpolation = React.useMemo(() => {
+ return createInterpolation({
+ firstColor: actions.rightFirst?.color,
+ secondColor: actions.rightSecond?.color,
+ firstThreshold: actions.rightFirst?.threshold,
+ secondThreshold: actions.rightSecond?.threshold,
+ side: 'right',
+ })
+ }, [actions.rightFirst, actions.rightSecond])
+
+ const interpolation = React.useMemo<{
+ inputRange: number[]
+ outputRange: ColorValue[]
+ }>(() => {
+ if (!actions.leftFirst) {
+ return rightSideInterpolation!
+ } else if (!actions.rightFirst) {
+ return leftSideInterpolation!
+ } else {
+ return {
+ inputRange: [
+ ...leftSideInterpolation.inputRange,
+ ...rightSideInterpolation.inputRange,
+ ],
+ outputRange: [
+ ...leftSideInterpolation.outputRange,
+ ...rightSideInterpolation.outputRange,
+ ],
+ }
+ }
+ }, [
+ leftSideInterpolation,
+ rightSideInterpolation,
+ actions.leftFirst,
+ actions.rightFirst,
+ ])
+
+ const animatedBackgroundStyle = useAnimatedStyle(() => {
+ return {
+ backgroundColor: interpolateColor(
+ clampedTransX.value,
+ interpolation.inputRange,
+ // @ts-expect-error - Weird type expected by reanimated, but this is okay
+ interpolation.outputRange,
+ ),
+ }
+ })
+
+ const animatedIconStyle = useAnimatedStyle(() => {
+ const absTransX = Math.abs(clampedTransX.value)
+ return {
+ opacity: interpolate(absTransX, [0, 75], [0.15, 1]),
+ transform: [{scale: iconScale.value}],
+ }
+ })
+
+ return (
+
( Mobile: React.ComponentType
, diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 1cad5e0910..a87af1a696 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -17,11 +17,13 @@ import { import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {IS_INTERNAL} from '#/lib/app-info' import {POST_CTRL_HITSLOP} from '#/lib/constants' +import {CountWheel} from '#/lib/custom-animations/CountWheel' +import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' import {useHaptics} from '#/lib/haptics' import {makeProfileLink} from '#/lib/routes/links' import {shareUrl} from '#/lib/sharing' -import {useGate} from '#/lib/statsig/statsig' import {toShareUrl} from '#/lib/strings/url-helpers' import {Shadow} from '#/state/cache/types' import {useFeedFeedbackContext} from '#/state/feed-feedback' @@ -35,8 +37,6 @@ import { ProgressGuideAction, useProgressGuideControls, } from '#/state/shell/progress-guide' -import {CountWheel} from 'lib/custom-animations/CountWheel' -import {AnimatedLikeIcon} from 'lib/custom-animations/LikeIcon' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox' @@ -85,7 +85,6 @@ let PostCtrls = ({ const {sendInteraction} = useFeedFeedbackContext() const {captureAction} = useProgressGuideControls() const playHaptic = useHaptics() - const gate = useGate() const isBlocked = Boolean( post.author.viewer?.blocking || post.author.viewer?.blockedBy || @@ -120,7 +119,7 @@ let PostCtrls = ({ try { setIsToggleLikeIcon(true) if (!post.viewer?.like) { - playHaptic() + playHaptic('Light') sendInteraction({ item: post.uri, event: 'app.bsky.feed.defs#interactionLike', @@ -200,27 +199,15 @@ let PostCtrls = ({ feedContext, }) openComposer({ - quote: { - uri: post.uri, - cid: post.cid, - text: record.text, - author: post.author, - indexedAt: post.indexedAt, - }, - quoteCount: post.quoteCount, + quote: post, onPost: onPostReply, }) }, [ _, sendInteraction, - post.uri, - post.cid, - post.author, - post.indexedAt, - post.quoteCount, + post, feedContext, openComposer, - record.text, onPostReply, isBlocked, ]) @@ -263,6 +250,7 @@ let PostCtrls = ({ style={btnStyle} onPress={() => { if (!post.viewer?.replyDisabled) { + playHaptic('Light') requireAuth(() => onPressReply()) } }} @@ -375,7 +363,7 @@ let PostCtrls = ({ threadgateRecord={threadgateRecord} />
A basic dialog
Will unmount in about 5 seconds
diff --git a/src/view/screens/Storybook/ListContained.tsx b/src/view/screens/Storybook/ListContained.tsx index 20ec686570..8333201482 100644 --- a/src/view/screens/Storybook/ListContained.tsx +++ b/src/view/screens/Storybook/ListContained.tsx @@ -1,8 +1,8 @@ import React from 'react' import {FlatList, View} from 'react-native' -import {ScrollProvider} from 'lib/ScrollContext' -import {List} from 'view/com/util/List' +import {ScrollProvider} from '#/lib/ScrollContext' +import {List} from '#/view/com/util/List' import {Button, ButtonText} from '#/components/Button' import * as Toggle from '#/components/forms/Toggle' import {Text} from '#/components/Typography' diff --git a/src/view/screens/Storybook/Menus.tsx b/src/view/screens/Storybook/Menus.tsx index 2f2b147215..3e5c74d86e 100644 --- a/src/view/screens/Storybook/Menus.tsx +++ b/src/view/screens/Storybook/Menus.tsx @@ -2,9 +2,9 @@ import React from 'react' import {View} from 'react-native' import {atoms as a, useTheme} from '#/alf' -import {Text} from '#/components/Typography' -import * as Menu from '#/components/Menu' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' +import * as Menu from '#/components/Menu' +import {Text} from '#/components/Typography' // import {useDialogStateControlContext} from '#/state/dialogs' export function Menus() { diff --git a/src/view/screens/Storybook/Settings.tsx b/src/view/screens/Storybook/Settings.tsx new file mode 100644 index 0000000000..6bc293c73c --- /dev/null +++ b/src/view/screens/Storybook/Settings.tsx @@ -0,0 +1,134 @@ +import React from 'react' +import {View} from 'react-native' + +import * as Toast from '#/view/com/util/Toast' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a, useTheme} from '#/alf' +import {Alien_Stroke2_Corner0_Rounded as AlienIcon} from '#/components/icons/Alien' +import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake' +import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' +import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' +import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope' +import {Explosion_Stroke2_Corner0_Rounded as ExplosionIcon} from '#/components/icons/Explosion' +import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' +import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' +import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' +import {Pizza_Stroke2_Corner0_Rounded as PizzaIcon} from '#/components/icons/Pizza' +import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' +import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' +import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' +import {Text} from '#/components/Typography' + +export function Settings() { + const t = useTheme() + return ( +