Skip to content

Commit

Permalink
feat(rn-list): manage list
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Jan 24, 2025
1 parent 527f6ac commit e3baee3
Show file tree
Hide file tree
Showing 13 changed files with 348 additions and 96 deletions.
206 changes: 124 additions & 82 deletions apps/mobile/src/components/common/SwipeableItem.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useTypeScriptHappyCallback } from "@follow/hooks"
import { impactAsync, ImpactFeedbackStyle } from "expo-haptics"
import { atom, useAtomValue, useSetAtom } from "jotai"
import { selectAtom } from "jotai/utils"
import * as React from "react"
Expand All @@ -17,6 +19,8 @@ interface SwipeableItemProps {
leftActions?: Action[]
rightActions?: Action[]
disabled?: boolean

swipeRightToCallAction?: boolean
}

const styles = StyleSheet.create({
Expand All @@ -41,18 +45,22 @@ const styles = StyleSheet.create({
},
})

const rectButtonWidth = 74

export const SwipeableItem: React.FC<SwipeableItemProps> = ({
children,
leftActions,
rightActions,
disabled,

swipeRightToCallAction,
}) => {
const [leftHaptic, setLeftHaptic] = React.useState(false)
const [rightHaptic, setRightHaptic] = React.useState(false)
const itemRef = React.useRef<Swipeable | null>(null)

const endDragCallerRef = React.useRef<() => void>(() => {})

const renderLeftActions = (progress: Animated.AnimatedInterpolation<number>) => {
const width = leftActions?.length ? leftActions.length * 74 : 74
const width = leftActions?.length ? leftActions.length * rectButtonWidth : rectButtonWidth

return (
<>
Expand All @@ -68,28 +76,18 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
{leftActions?.map((action, index) => {
const trans = progress.interpolate({
inputRange: [0, 1],
outputRange: [-74 * (leftHaptic ? (leftActions?.length ?? 1) : index + 1), 0],
outputRange: [-rectButtonWidth * (leftActions?.length ?? 1), 0],
})

if (index === 0) {
trans.addListener(({ value }) => {
if (value >= (leftActions?.length === 1 ? 40 : 20)) {
setLeftHaptic(true)
} else {
leftHaptic && setLeftHaptic(false)
}
})
}

return (
<Animated.View
key={index}
style={[
styles.animatedContainer,
{
transform: [{ translateX: trans }],
width: leftHaptic && index === 0 ? "100%" : 74,
left: index * 74,
width: rectButtonWidth,
left: index * rectButtonWidth,
},
]}
>
Expand All @@ -116,12 +114,8 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
}

const renderRightActions = (progress: Animated.AnimatedInterpolation<number>) => {
const width = rightActions?.length ? rightActions.length * 74 : 74
const width = rightActions?.length ? rightActions.length * rectButtonWidth : rectButtonWidth

const parallaxX = progress.interpolate({
inputRange: [0, 1, 1.2],
outputRange: [0, 0, 10],
})
return (
<>
<View
Expand All @@ -134,54 +128,18 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
/>
<Animated.View style={[styles.actionsWrapper, { width }]}>
{rightActions?.map((action, index) => {
const trans = progress.interpolate({
inputRange: [0, 1],
outputRange: [74 * (rightHaptic ? (rightActions?.length ?? 1) : index + 1), 0],
})

if (index === 0) {
trans.addListener(({ value }) => {
if (value <= (rightActions?.length === 1 ? -40 : -20)) {
setRightHaptic(true)
} else {
rightHaptic && setRightHaptic(false)
}
})
}

return (
<Animated.View
<RightRectButton
endDragCallerRef={endDragCallerRef}
key={index}
style={[
styles.animatedContainer,
{
transform: [{ translateX: trans }],
width: rightHaptic && index === 0 ? "100%" : 74,
left: index * 74,
},
]}
>
<RectButton
style={[
styles.actionContainer,
{
backgroundColor: action.backgroundColor ?? "#fff",
},
]}
onPress={action.onPress}
>
{action.icon}
<Animated.Text
style={[
styles.actionText,
{ color: action.color ?? "#fff" },
{ transform: [{ translateX: parallaxX }] },
]}
>
{action.label}
</Animated.Text>
</RectButton>
</Animated.View>
index={index}
action={action}
length={rightActions?.length ?? 1}
progress={progress}
swipeRightToCallAction={
swipeRightToCallAction && index === rightActions?.length - 1
}
/>
)
})}
</Animated.View>
Expand Down Expand Up @@ -218,26 +176,16 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
rightThreshold={37}
enableTrackpadTwoFingerGesture
useNativeAnimations
onEnded={(e: any) => {
const { translationX } = e.nativeEvent
if (
leftHaptic &&
translationX >= (leftActions?.length === 1 ? 100 : 60) * (leftActions?.length ?? 1)
) {
leftActions?.[0]?.onPress?.()
}
if (
rightHaptic &&
translationX <= (rightActions?.length === 1 ? -100 : -60) * (rightActions?.length ?? 1)
) {
rightActions?.[0]?.onPress?.()
onEnded={useTypeScriptHappyCallback(() => {
if (swipeRightToCallAction && endDragCallerRef.current) {
endDragCallerRef.current()
}
}}
}, [swipeRightToCallAction, endDragCallerRef])}
renderLeftActions={leftActions?.length ? renderLeftActions : undefined}
renderRightActions={rightActions?.length ? renderRightActions : undefined}
overshootLeft={leftActions?.length ? leftActions?.length >= 1 : undefined}
overshootRight={rightActions?.length ? rightActions?.length >= 1 : undefined}
overshootFriction={10}
overshootFriction={swipeRightToCallAction ? 1 : 10}
>
{children}
</Swipeable>
Expand All @@ -258,3 +206,97 @@ export const SwipeableGroupProvider = ({ children }: { children: React.ReactNode

return <SwipeableGroupContext.Provider value={ctx}>{children}</SwipeableGroupContext.Provider>
}

const rightActionThreshold = -100
const RightRectButton = React.memo(
({
index,
action,
length = 1,
progress,
swipeRightToCallAction,
endDragCallerRef,
}: {
progress: Animated.AnimatedInterpolation<number>
index: number
action: Action
length: number
swipeRightToCallAction?: boolean
endDragCallerRef: React.MutableRefObject<() => void>
}) => {
const trans = React.useMemo(
() =>
progress.interpolate({
inputRange: [0, 1, 1.2],
outputRange: [rectButtonWidth * length, 0, -40],
}),
[progress, length],
)
const parallaxX = React.useMemo(
() =>
progress.interpolate({
inputRange: [0, 1, 1.2],
outputRange: [0, 0, 10],
}),
[progress],
)

const hapticOnce = React.useRef(false)

React.useEffect(() => {
if (!swipeRightToCallAction) return
const id = trans.addListener(({ value }) => {
if (value <= rightActionThreshold) {
if (hapticOnce.current) return
hapticOnce.current = true
impactAsync(ImpactFeedbackStyle.Light)
endDragCallerRef.current = () => {
action.onPress?.()
}
} else {
hapticOnce.current = false
endDragCallerRef.current = () => {}
}
})

return () => {
trans.removeListener(id)
}
}, [action, endDragCallerRef, swipeRightToCallAction, trans])

return (
<Animated.View
key={index}
style={[
styles.animatedContainer,
{
transform: [{ translateX: trans }],
width: rectButtonWidth,
left: index * rectButtonWidth,
},
]}
>
<RectButton
style={[
styles.actionContainer,
{
backgroundColor: action.backgroundColor ?? "#fff",
},
]}
onPress={action.onPress}
>
{action.icon}
<Animated.Text
style={[
styles.actionText,
{ color: action.color ?? "#fff" },
{ transform: [{ translateX: parallaxX }] },
]}
>
{action.label}
</Animated.Text>
</RectButton>
</Animated.View>
)
},
)
1 change: 1 addition & 0 deletions apps/mobile/src/components/ui/grouped/GroupedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const GroupedInsetListCard: FC<
? React.Children.map(children, (child, index) => {
const isLast = index === React.Children.count(children) - 1

if (child === null) return null
const isNavigationLink =
React.isValidElement(child) &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
Expand Down
7 changes: 5 additions & 2 deletions apps/mobile/src/modules/settings/routes/Lists.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type { ListModel } from "@/src/store/list/store"
import { accentColor } from "@/src/theme/colors"

import { SwipeableGroupProvider, SwipeableItem } from "../../../components/common/SwipeableItem"
import { useSettingsNavigation } from "../hooks"

const ListContext = createContext({} as Record<string, HonoApiClient.List_List_Get>)
export const ListsScreen = () => {
Expand Down Expand Up @@ -122,13 +123,15 @@ const ListItemCell: ListRenderItem<ListModel> = (props) => {
const ListItemCellImpl: ListRenderItem<ListModel> = ({ item: list }) => {
const { title, description } = list
const listData = useContext(ListContext)[list.id]
const navigation = useSettingsNavigation()
return (
<SwipeableItem
swipeRightToCallAction
rightActions={[
{
label: "Manage",
onPress: () => {
router.push(`/manage-list?id=${list.id}`)
navigation.navigate("ManageList", { id: list.id })
},
backgroundColor: accentColor,
},
Expand All @@ -143,7 +146,7 @@ const ListItemCellImpl: ListRenderItem<ListModel> = ({ item: list }) => {
>
<ItemPressable
className="flex-row p-4"
onPress={() => router.push(`/manage-list?id=${list.id}`)}
onPress={() => navigation.navigate("ManageList", { id: list.id })}
>
<View className="size-16 overflow-hidden rounded-lg">
{list.image ? (
Expand Down
Loading

0 comments on commit e3baee3

Please sign in to comment.