Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 168 lines 5.1 kB view raw
1import {useState} from 'react' 2import {View} from 'react-native' 3 4import {useTheme} from '#/alf' 5import {DotGrid2x3_Stroke2_Corner0_Rounded as GripIcon} from '#/components/icons/DotGrid' 6 7/** 8 * Web implementation of SortableList using pointer events. 9 * See SortableList.tsx for the native version using gesture-handler + Reanimated. 10 */ 11 12interface SortableListProps<T> { 13 data: T[] 14 keyExtractor: (item: T) => string 15 renderItem: (item: T, dragHandle: React.ReactNode) => React.ReactNode 16 onReorder: (data: T[]) => void 17 onDragStart?: () => void 18 onDragEnd?: () => void 19 /** Fixed row height used for position math. */ 20 itemHeight: number 21} 22 23export function SortableList<T>({ 24 data, 25 keyExtractor, 26 renderItem, 27 onReorder, 28 onDragStart, 29 onDragEnd, 30 itemHeight, 31}: SortableListProps<T>) { 32 const t = useTheme() 33 const [dragState, setDragState] = useState<{ 34 activeIndex: number 35 currentY: number 36 startY: number 37 } | null>(null) 38 39 const getNewPosition = (state: { 40 activeIndex: number 41 currentY: number 42 startY: number 43 }) => { 44 const translationY = state.currentY - state.startY 45 const rawNewPos = Math.round( 46 (state.activeIndex * itemHeight + translationY) / itemHeight, 47 ) 48 return Math.max(0, Math.min(rawNewPos, data.length - 1)) 49 } 50 51 const handlePointerMove = (e: React.PointerEvent) => { 52 if (!dragState) return 53 e.preventDefault() 54 setDragState(prev => (prev ? {...prev, currentY: e.clientY} : null)) 55 } 56 57 const handlePointerUp = () => { 58 if (!dragState) return 59 const newPos = getNewPosition(dragState) 60 if (newPos !== dragState.activeIndex) { 61 const next = [...data] 62 const [moved] = next.splice(dragState.activeIndex, 1) 63 next.splice(newPos, 0, moved) 64 onReorder(next) 65 } 66 setDragState(null) 67 onDragEnd?.() 68 } 69 70 const handlePointerDown = (e: React.PointerEvent, index: number) => { 71 e.preventDefault() 72 ;(e.target as HTMLElement).setPointerCapture(e.pointerId) 73 setDragState({activeIndex: index, currentY: e.clientY, startY: e.clientY}) 74 onDragStart?.() 75 } 76 77 const newPos = dragState ? getNewPosition(dragState) : -1 78 79 return ( 80 <View 81 style={[ 82 {height: data.length * itemHeight, position: 'relative'}, 83 t.atoms.bg_contrast_25, 84 ]} 85 // @ts-expect-error web-only pointer events 86 onPointerMove={handlePointerMove} 87 onPointerUp={handlePointerUp} 88 onPointerCancel={handlePointerUp}> 89 {data.map((item, index) => { 90 const isActive = dragState?.activeIndex === index 91 92 // Clamp translation so the item stays within list bounds. 93 const rawTranslationY = isActive 94 ? dragState.currentY - dragState.startY 95 : 0 96 const translationY = isActive 97 ? Math.max( 98 -index * itemHeight, 99 Math.min(rawTranslationY, (data.length - 1 - index) * itemHeight), 100 ) 101 : 0 102 103 // Non-dragged items shift to make room for the dragged item. 104 let offset = 0 105 if (dragState && !isActive) { 106 const orig = dragState.activeIndex 107 if (orig < newPos && index > orig && index <= newPos) { 108 offset = -itemHeight 109 } else if (orig > newPos && index < orig && index >= newPos) { 110 offset = itemHeight 111 } 112 } 113 114 const dragHandle = ( 115 <div 116 onPointerDown={(e: React.PointerEvent<HTMLDivElement>) => 117 handlePointerDown(e, index) 118 } 119 style={{ 120 display: 'flex', 121 justifyContent: 'center', 122 alignItems: 'center', 123 paddingLeft: 8, 124 paddingRight: 8, 125 paddingTop: 12, 126 paddingBottom: 12, 127 cursor: isActive ? 'grabbing' : 'grab', 128 touchAction: 'none', 129 userSelect: 'none', 130 }}> 131 <GripIcon 132 size="lg" 133 fill={t.atoms.text_contrast_medium.color} 134 style={{pointerEvents: 'none'} as any} 135 /> 136 </div> 137 ) 138 139 return ( 140 <View 141 key={keyExtractor(item)} 142 style={[ 143 { 144 position: 'absolute', 145 top: index * itemHeight, 146 left: 0, 147 right: 0, 148 height: itemHeight, 149 transform: [{translateY: isActive ? translationY : offset}], 150 scale: isActive ? 1.03 : 1, 151 zIndex: isActive ? 999 : 0, 152 boxShadow: isActive ? '0 2px 12px rgba(0,0,0,0.06)' : 'none', 153 // Animate scale/shadow on pickup, and transform for 154 // non-dragged items shifting into place. 155 transition: isActive 156 ? 'box-shadow 200ms ease, scale 200ms ease' 157 : dragState 158 ? 'transform 200ms ease' 159 : 'none', 160 } as any, 161 ]}> 162 {renderItem(item, dragHandle)} 163 </View> 164 ) 165 })} 166 </View> 167 ) 168}