mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

Working overlay, WIP

Eric Bailey 19ec70b8 c894dda0

+293 -1
+262
src/components/Tooltip/index.tsx
··· 1 + import { 2 + useMemo, 3 + useRef, 4 + createContext, 5 + useContext, 6 + useState, 7 + isValidElement, 8 + } from 'react' 9 + import {View, Modal, Dimensions, Pressable} from 'react-native' 10 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 11 + import flattenReactChildren from 'react-keyed-flatten-children' 12 + 13 + import {Portal} from '#/components/Portal' 14 + import {atoms as a, useTheme} from '#/alf' 15 + 16 + type Context = { 17 + visible: boolean 18 + onVisibleChange: (visible: boolean) => void 19 + targetMeasurements: 20 + | { 21 + x: number 22 + y: number 23 + width: number 24 + height: number 25 + } 26 + | undefined 27 + targetRef: React.RefObject<View> 28 + targetElement: any 29 + } 30 + 31 + const TooltipContext = createContext<Context>({ 32 + visible: false, 33 + onVisibleChange: () => {}, 34 + targetMeasurements: undefined, 35 + targetRef: {current: null}, 36 + targetElement: null, 37 + }) 38 + 39 + export function Outer({ 40 + children, 41 + visible: requestVisible, 42 + onVisibleChange, 43 + }: { 44 + children: React.ReactNode 45 + visible: boolean 46 + onVisibleChange: (visible: boolean) => void 47 + }) { 48 + const targetRef = useRef<View>(null) 49 + const [prevRequestVisible, setPrevRequestVisible] = useState< 50 + boolean | undefined 51 + >() 52 + const [visible, setVisible] = useState(false) 53 + const [measurements, setMeasurements] = useState< 54 + | { 55 + x: number 56 + y: number 57 + width: number 58 + height: number 59 + } 60 + | undefined 61 + >(undefined) 62 + 63 + if (requestVisible && !prevRequestVisible) { 64 + setPrevRequestVisible(requestVisible) 65 + if (targetRef.current) { 66 + targetRef.current.measure((_x, _y, width, height, pageX, pageY) => { 67 + setMeasurements({x: pageX, y: pageY, width, height}) 68 + setVisible(true) 69 + }) 70 + } 71 + } else if (!requestVisible && prevRequestVisible) { 72 + setPrevRequestVisible(requestVisible) 73 + setMeasurements(undefined) 74 + setVisible(false) 75 + } 76 + 77 + const ctx = useMemo( 78 + () => ({ 79 + visible, 80 + onVisibleChange, 81 + targetMeasurements: measurements, 82 + targetRef, 83 + targetElement: flattenReactChildren(children).find(child => { 84 + if (isValidElement(child) && child.type === Target) { 85 + return true 86 + } 87 + }), 88 + }), 89 + [visible, onVisibleChange, measurements, targetRef], 90 + ) 91 + 92 + return ( 93 + <TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider> 94 + ) 95 + } 96 + 97 + export function Target({ 98 + children, 99 + }: { 100 + children: (props: {ref: Context['targetRef']}) => React.ReactNode 101 + }) { 102 + const {targetRef} = useContext(TooltipContext) 103 + 104 + return children({ 105 + ref: targetRef, 106 + }) 107 + } 108 + 109 + const TIP_SIZE = 12 110 + 111 + export function Content({children}: {children: React.ReactNode}) { 112 + const t = useTheme() 113 + const {visible, onVisibleChange, targetMeasurements, targetElement} = 114 + useContext(TooltipContext) 115 + const [ready, setReady] = useState(false) 116 + const [measurements, setMeasurements] = useState< 117 + | { 118 + width: number 119 + height: number 120 + } 121 + | undefined 122 + >(undefined) 123 + const safe = useSafeAreaInsets() 124 + 125 + const requestClose = () => { 126 + onVisibleChange(false) 127 + setReady(false) 128 + setMeasurements(undefined) 129 + } 130 + 131 + const {positionTop, contentLeft, tipTop, tipLeft} = useMemo(() => { 132 + if (!targetMeasurements || !measurements) 133 + return { 134 + positionTop: 0, 135 + contentLeft: 0, 136 + tipTop: 0, 137 + tipLeft: 0, 138 + } 139 + 140 + const win = Dimensions.get('window') 141 + const {width: ww, height: wh} = win 142 + const maxTop = safe.top 143 + const maxBottom = wh - safe.bottom 144 + const {width: cw, height: ch} = measurements 145 + const minLeft = a.px_xl.paddingLeft 146 + const maxLeft = ww - minLeft 147 + 148 + let positionTop = targetMeasurements.y + targetMeasurements.height 149 + let contentLeft = Math.max( 150 + minLeft, 151 + targetMeasurements.x + targetMeasurements.width / 2 - cw / 2, 152 + ) 153 + let tipTop = (TIP_SIZE / 2) * -1 154 + let tipLeft = 155 + targetMeasurements.x + targetMeasurements.width / 2 - TIP_SIZE / 2 156 + 157 + if (contentLeft + cw > maxLeft) { 158 + contentLeft -= contentLeft + cw - maxLeft 159 + } 160 + 161 + positionTop += TIP_SIZE / 3 162 + 163 + return { 164 + positionTop, 165 + contentLeft, 166 + tipTop, 167 + tipLeft, 168 + } 169 + }, [targetMeasurements, measurements, safe]) 170 + 171 + if (!visible || !targetMeasurements) return null 172 + 173 + return ( 174 + <Portal> 175 + <Modal 176 + transparent 177 + onRequestClose={() => {}} // TODO 178 + style={[a.absolute, a.inset_0]}> 179 + {/* Backdrop */} 180 + <Pressable 181 + style={[ 182 + a.absolute, 183 + a.inset_0, 184 + t.atoms.bg, 185 + { 186 + opacity: 0.4, 187 + }, 188 + ]} 189 + onPress={requestClose} 190 + /> 191 + 192 + {/* Replica of the target element */} 193 + <View 194 + style={[ 195 + a.absolute, 196 + { 197 + top: targetMeasurements.y, 198 + left: targetMeasurements.x, 199 + height: targetMeasurements.height, 200 + width: targetMeasurements.width, 201 + }, 202 + ]}> 203 + {targetElement} 204 + </View> 205 + 206 + {/* Outer content container */} 207 + <View 208 + style={[ 209 + a.absolute, 210 + a.align_start, 211 + { 212 + opacity: ready ? 1 : 0, 213 + top: positionTop, 214 + left: 0, 215 + right: 0, 216 + }, 217 + ]}> 218 + {/* The triangle "tip" */} 219 + <View 220 + style={[ 221 + a.absolute, 222 + a.top_0, 223 + a.z_10, 224 + t.atoms.bg, 225 + { 226 + borderTopLeftRadius: a.rounded_xs.borderRadius, 227 + width: TIP_SIZE, 228 + height: TIP_SIZE, 229 + transform: [{rotate: '45deg'}], 230 + top: tipTop, 231 + left: tipLeft, 232 + }, 233 + ]} 234 + /> 235 + 236 + {/* The content itself */} 237 + <View 238 + style={[ 239 + a.px_md, 240 + a.py_sm, 241 + a.rounded_sm, 242 + t.atoms.bg, 243 + t.atoms.shadow_sm, 244 + { 245 + maxWidth: 200, 246 + left: contentLeft, 247 + }, 248 + ]} 249 + onLayout={e => { 250 + setReady(true) 251 + setMeasurements({ 252 + width: e.nativeEvent.layout.width, 253 + height: e.nativeEvent.layout.height, 254 + }) 255 + }}> 256 + {children} 257 + </View> 258 + </View> 259 + </Modal> 260 + </Portal> 261 + ) 262 + }
+31 -1
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 40 40 import {ProfileHeaderHandle} from './Handle' 41 41 import {ProfileHeaderMetrics} from './Metrics' 42 42 import {ProfileHeaderShell} from './Shell' 43 + import * as Tooltip from '#/components/Tooltip' 43 44 44 45 interface Props { 45 46 profile: AppBskyActorDefs.ProfileViewDetailed ··· 141 142 142 143 const {isActive: live} = useActorStatus(profile) 143 144 145 + const [visible, setVisible] = React.useState(false) 146 + 144 147 return ( 145 148 <ProfileHeaderShell 146 149 profile={profile} ··· 198 201 ) 199 202 ) : !profile.viewer?.blockedBy ? ( 200 203 <> 201 - {hasSession && <MessageProfileButton profile={profile} />} 204 + <Button 205 + label='test' 206 + size='small' 207 + variant='solid' 208 + color='secondary' 209 + shape='round' 210 + onPress={() => setVisible(!visible)} 211 + > 212 + <ButtonIcon position="left" icon={Plus} /> 213 + </Button> 202 214 203 215 <Button 204 216 testID={profile.viewer?.following ? 'unfollowBtn' : 'followBtn'} ··· 227 239 )} 228 240 </ButtonText> 229 241 </Button> 242 + 243 + 244 + {hasSession && ( 245 + <> 246 + <Tooltip.Outer visible={visible} onVisibleChange={setVisible}> 247 + <Tooltip.Target> 248 + {({ref}) => ( 249 + <View ref={ref}> 250 + <MessageProfileButton profile={profile} /> 251 + </View> 252 + )} 253 + </Tooltip.Target> 254 + <Tooltip.Content> 255 + <Text>The quick brown fox jumps over the lazy dog</Text> 256 + </Tooltip.Content> 257 + </Tooltip.Outer> 258 + </> 259 + )} 230 260 </> 231 261 ) : null} 232 262 <ProfileMenu profile={profile} />