mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 06:34:24 +01:00
feat(ui): replace custom pull-to-refresh with use-pull-to-refresh library for simplicity
- Remove custom usePullToRefresh hook and PullToRefreshIndicator - Add use-pull-to-refresh library dependency - Create simple RefreshIndicator component - Apply pull-to-refresh to Explore and Me screens - Simplify implementation while maintaining functionality
This commit is contained in:
@@ -4,3 +4,5 @@ alwaysApply: false
|
|||||||
---
|
---
|
||||||
|
|
||||||
This is a mobile-first application. All UI elements should be designed with that in mind. The application should work well on small screens, including older smartphones. The UX should be immaculate on mobile, even when in flight mode. (We use local caches and local relays, so that app works offline too.)
|
This is a mobile-first application. All UI elements should be designed with that in mind. The application should work well on small screens, including older smartphones. The UX should be immaculate on mobile, even when in flight mode. (We use local caches and local relays, so that app works offline too.)
|
||||||
|
|
||||||
|
Let's not show too many error messages, and more importantly: let's not make them red. Nothing is ever this tragic.
|
||||||
|
|||||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "boris",
|
"name": "boris",
|
||||||
"version": "0.6.6",
|
"version": "0.6.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "boris",
|
"name": "boris",
|
||||||
"version": "0.6.6",
|
"version": "0.6.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||||
@@ -33,7 +33,8 @@
|
|||||||
"reading-time-estimator": "^1.14.0",
|
"reading-time-estimator": "^1.14.0",
|
||||||
"rehype-prism-plus": "^2.0.1",
|
"rehype-prism-plus": "^2.0.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.1"
|
"remark-gfm": "^4.0.1",
|
||||||
|
"use-pull-to-refresh": "^2.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.14",
|
"@tailwindcss/postcss": "^4.1.14",
|
||||||
@@ -11695,6 +11696,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-pull-to-refresh": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-pull-to-refresh/-/use-pull-to-refresh-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-mI3utetwSPT3ovZHUJ4LBW29EtmkrzpK/O38msP5WnI8ocFmM5boy3QZALosgeQwqwdmtQgC+8xnJIYHXeABew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "18.x || 19.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
|||||||
@@ -36,7 +36,8 @@
|
|||||||
"reading-time-estimator": "^1.14.0",
|
"reading-time-estimator": "^1.14.0",
|
||||||
"rehype-prism-plus": "^2.0.1",
|
"rehype-prism-plus": "^2.0.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.1"
|
"remark-gfm": "^4.0.1",
|
||||||
|
"use-pull-to-refresh": "^2.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.14",
|
"@tailwindcss/postcss": "^4.1.14",
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import { UserSettings } from '../services/settingsService'
|
|||||||
import BlogPostCard from './BlogPostCard'
|
import BlogPostCard from './BlogPostCard'
|
||||||
import { HighlightItem } from './HighlightItem'
|
import { HighlightItem } from './HighlightItem'
|
||||||
import { getCachedPosts, upsertCachedPost, setCachedPosts, getCachedHighlights, upsertCachedHighlight, setCachedHighlights } from '../services/exploreCache'
|
import { getCachedPosts, upsertCachedPost, setCachedPosts, getCachedHighlights, upsertCachedHighlight, setCachedHighlights } from '../services/exploreCache'
|
||||||
import { usePullToRefresh } from '../hooks/usePullToRefresh'
|
import { usePullToRefresh } from 'use-pull-to-refresh'
|
||||||
import PullToRefreshIndicator from './PullToRefreshIndicator'
|
import RefreshIndicator from './RefreshIndicator'
|
||||||
import { classifyHighlights } from '../utils/highlightClassification'
|
import { classifyHighlights } from '../utils/highlightClassification'
|
||||||
import { HighlightVisibility } from './HighlightsPanel'
|
import { HighlightVisibility } from './HighlightsPanel'
|
||||||
|
|
||||||
@@ -41,7 +41,6 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
|
|||||||
const [followedPubkeys, setFollowedPubkeys] = useState<Set<string>>(new Set())
|
const [followedPubkeys, setFollowedPubkeys] = useState<Set<string>>(new Set())
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const exploreContainerRef = useRef<HTMLDivElement>(null)
|
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
||||||
|
|
||||||
// Visibility filters (defaults from settings)
|
// Visibility filters (defaults from settings)
|
||||||
@@ -229,11 +228,13 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
|
|||||||
}, [relayPool, activeAccount, refreshTrigger, eventStore, settings])
|
}, [relayPool, activeAccount, refreshTrigger, eventStore, settings])
|
||||||
|
|
||||||
// Pull-to-refresh
|
// Pull-to-refresh
|
||||||
const pullToRefreshState = usePullToRefresh(exploreContainerRef, {
|
const { isRefreshing, pullPosition } = usePullToRefresh({
|
||||||
onRefresh: () => {
|
onRefresh: () => {
|
||||||
setRefreshTrigger(prev => prev + 1)
|
setRefreshTrigger(prev => prev + 1)
|
||||||
},
|
},
|
||||||
isRefreshing: loading
|
maximumPullLength: 240,
|
||||||
|
refreshThreshold: 80,
|
||||||
|
isDisabled: !activeAccount
|
||||||
})
|
})
|
||||||
|
|
||||||
const getPostUrl = (post: BlogPostPreview) => {
|
const getPostUrl = (post: BlogPostPreview) => {
|
||||||
@@ -393,15 +394,10 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="explore-container">
|
||||||
ref={exploreContainerRef}
|
<RefreshIndicator
|
||||||
className={`explore-container pull-to-refresh-container ${pullToRefreshState.isPulling ? 'is-pulling' : ''}`}
|
isRefreshing={isRefreshing}
|
||||||
>
|
pullPosition={pullPosition}
|
||||||
<PullToRefreshIndicator
|
|
||||||
isPulling={pullToRefreshState.isPulling}
|
|
||||||
pullDistance={pullToRefreshState.pullDistance}
|
|
||||||
canRefresh={pullToRefreshState.canRefresh}
|
|
||||||
isRefreshing={loading && pullToRefreshState.canRefresh}
|
|
||||||
/>
|
/>
|
||||||
<div className="explore-header">
|
<div className="explore-header">
|
||||||
<h1>
|
<h1>
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import { ViewMode } from './Bookmarks'
|
|||||||
import { extractUrlsFromContent } from '../services/bookmarkHelpers'
|
import { extractUrlsFromContent } from '../services/bookmarkHelpers'
|
||||||
import { getCachedMeData, setCachedMeData, updateCachedHighlights } from '../services/meCache'
|
import { getCachedMeData, setCachedMeData, updateCachedHighlights } from '../services/meCache'
|
||||||
import { faBooks } from '../icons/customIcons'
|
import { faBooks } from '../icons/customIcons'
|
||||||
import { usePullToRefresh } from '../hooks/usePullToRefresh'
|
import { usePullToRefresh } from 'use-pull-to-refresh'
|
||||||
import PullToRefreshIndicator from './PullToRefreshIndicator'
|
import RefreshIndicator from './RefreshIndicator'
|
||||||
import { getProfileUrl } from '../config/nostrGateways'
|
import { getProfileUrl } from '../config/nostrGateways'
|
||||||
|
|
||||||
interface MeProps {
|
interface MeProps {
|
||||||
@@ -49,7 +49,6 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('cards')
|
const [viewMode, setViewMode] = useState<ViewMode>('cards')
|
||||||
const meContainerRef = useRef<HTMLDivElement>(null)
|
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
||||||
|
|
||||||
// Update local state when prop changes
|
// Update local state when prop changes
|
||||||
@@ -125,11 +124,13 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
|
|||||||
}, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger])
|
}, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger])
|
||||||
|
|
||||||
// Pull-to-refresh
|
// Pull-to-refresh
|
||||||
const pullToRefreshState = usePullToRefresh(meContainerRef, {
|
const { isRefreshing, pullPosition } = usePullToRefresh({
|
||||||
onRefresh: () => {
|
onRefresh: () => {
|
||||||
setRefreshTrigger(prev => prev + 1)
|
setRefreshTrigger(prev => prev + 1)
|
||||||
},
|
},
|
||||||
isRefreshing: loading
|
maximumPullLength: 240,
|
||||||
|
refreshThreshold: 80,
|
||||||
|
isDisabled: !viewingPubkey
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleHighlightDelete = (highlightId: string) => {
|
const handleHighlightDelete = (highlightId: string) => {
|
||||||
@@ -367,15 +368,10 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="explore-container">
|
||||||
ref={meContainerRef}
|
<RefreshIndicator
|
||||||
className={`explore-container pull-to-refresh-container ${pullToRefreshState.isPulling ? 'is-pulling' : ''}`}
|
isRefreshing={isRefreshing}
|
||||||
>
|
pullPosition={pullPosition}
|
||||||
<PullToRefreshIndicator
|
|
||||||
isPulling={pullToRefreshState.isPulling}
|
|
||||||
pullDistance={pullToRefreshState.pullDistance}
|
|
||||||
canRefresh={pullToRefreshState.canRefresh}
|
|
||||||
isRefreshing={loading && pullToRefreshState.canRefresh}
|
|
||||||
/>
|
/>
|
||||||
<div className="explore-header">
|
<div className="explore-header">
|
||||||
{viewingPubkey && <AuthorCard authorPubkey={viewingPubkey} clickable={false} />}
|
{viewingPubkey && <AuthorCard authorPubkey={viewingPubkey} clickable={false} />}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import { faArrowDown } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
interface PullToRefreshIndicatorProps {
|
|
||||||
isPulling: boolean
|
|
||||||
pullDistance: number
|
|
||||||
canRefresh: boolean
|
|
||||||
isRefreshing: boolean
|
|
||||||
threshold?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const PullToRefreshIndicator: React.FC<PullToRefreshIndicatorProps> = ({
|
|
||||||
isPulling,
|
|
||||||
pullDistance,
|
|
||||||
canRefresh,
|
|
||||||
threshold = 80
|
|
||||||
}) => {
|
|
||||||
// Only show when actively pulling, not when refreshing
|
|
||||||
if (!isPulling) return null
|
|
||||||
|
|
||||||
const opacity = Math.min(pullDistance / threshold, 1)
|
|
||||||
const rotation = (pullDistance / threshold) * 180
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="pull-to-refresh-indicator"
|
|
||||||
style={{
|
|
||||||
opacity,
|
|
||||||
transform: `translateY(${-20 + pullDistance / 2}px)`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="pull-to-refresh-icon"
|
|
||||||
style={{
|
|
||||||
transform: `rotate(${rotation}deg)`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faArrowDown}
|
|
||||||
style={{ color: canRefresh ? 'var(--accent-color, #3b82f6)' : 'var(--text-secondary)' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="pull-to-refresh-text">
|
|
||||||
{canRefresh ? 'Release to refresh' : 'Pull to refresh'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PullToRefreshIndicator
|
|
||||||
|
|
||||||
63
src/components/RefreshIndicator.tsx
Normal file
63
src/components/RefreshIndicator.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faArrowRotateRight } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
interface RefreshIndicatorProps {
|
||||||
|
isRefreshing: boolean
|
||||||
|
pullPosition: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const THRESHOLD = 80
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple pull-to-refresh visual indicator
|
||||||
|
*/
|
||||||
|
const RefreshIndicator: React.FC<RefreshIndicatorProps> = ({
|
||||||
|
isRefreshing,
|
||||||
|
pullPosition
|
||||||
|
}) => {
|
||||||
|
const isVisible = isRefreshing || pullPosition > 0
|
||||||
|
if (!isVisible) return null
|
||||||
|
|
||||||
|
const opacity = Math.min(pullPosition / THRESHOLD, 1)
|
||||||
|
const translateY = isRefreshing ? THRESHOLD / 3 : pullPosition / 3
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: `${translateY}px`,
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
zIndex: 30,
|
||||||
|
opacity,
|
||||||
|
transition: isRefreshing ? 'opacity 0.2s' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: 'var(--surface-secondary, #ffffff)',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowRotateRight}
|
||||||
|
style={{
|
||||||
|
transform: isRefreshing ? 'none' : `rotate(${pullPosition}deg)`,
|
||||||
|
color: 'var(--accent-color, #3b82f6)'
|
||||||
|
}}
|
||||||
|
className={isRefreshing ? 'fa-spin' : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RefreshIndicator
|
||||||
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { useEffect, useRef, useState, RefObject } from 'react'
|
|
||||||
import { useIsCoarsePointer } from './useMediaQuery'
|
|
||||||
|
|
||||||
interface UsePullToRefreshOptions {
|
|
||||||
onRefresh: () => void | Promise<void>
|
|
||||||
isRefreshing?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
threshold?: number // Distance in pixels to trigger refresh
|
|
||||||
resistance?: number // Resistance factor (higher = harder to pull)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PullToRefreshState {
|
|
||||||
isPulling: boolean
|
|
||||||
pullDistance: number
|
|
||||||
canRefresh: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to enable pull-to-refresh gesture on touch devices
|
|
||||||
* @param containerRef - Ref to the scrollable container element
|
|
||||||
* @param options - Configuration options
|
|
||||||
* @returns State of the pull gesture
|
|
||||||
*/
|
|
||||||
export function usePullToRefresh(
|
|
||||||
containerRef: RefObject<HTMLElement>,
|
|
||||||
options: UsePullToRefreshOptions
|
|
||||||
): PullToRefreshState {
|
|
||||||
const {
|
|
||||||
onRefresh,
|
|
||||||
isRefreshing = false,
|
|
||||||
disabled = false,
|
|
||||||
threshold = 80,
|
|
||||||
resistance = 2.5
|
|
||||||
} = options
|
|
||||||
|
|
||||||
const isTouch = useIsCoarsePointer()
|
|
||||||
const [pullState, setPullState] = useState<PullToRefreshState>({
|
|
||||||
isPulling: false,
|
|
||||||
pullDistance: 0,
|
|
||||||
canRefresh: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const touchStartY = useRef<number>(0)
|
|
||||||
const startScrollTop = useRef<number>(0)
|
|
||||||
const isDragging = useRef<boolean>(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const container = containerRef.current
|
|
||||||
if (!container || !isTouch || disabled || isRefreshing) return
|
|
||||||
|
|
||||||
const handleTouchStart = (e: TouchEvent) => {
|
|
||||||
// Only start if scrolled to top
|
|
||||||
const scrollTop = container.scrollTop
|
|
||||||
if (scrollTop <= 0) {
|
|
||||||
touchStartY.current = e.touches[0].clientY
|
|
||||||
startScrollTop.current = scrollTop
|
|
||||||
isDragging.current = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTouchMove = (e: TouchEvent) => {
|
|
||||||
if (!isDragging.current) return
|
|
||||||
|
|
||||||
const currentY = e.touches[0].clientY
|
|
||||||
const deltaY = currentY - touchStartY.current
|
|
||||||
const scrollTop = container.scrollTop
|
|
||||||
|
|
||||||
// Only pull down when at top and pulling down
|
|
||||||
if (scrollTop <= 0 && deltaY > 0) {
|
|
||||||
// Prevent default scroll behavior
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
// Apply resistance to make pulling feel natural
|
|
||||||
const distance = Math.min(deltaY / resistance, threshold * 1.5)
|
|
||||||
const canRefresh = distance >= threshold
|
|
||||||
|
|
||||||
setPullState({
|
|
||||||
isPulling: true,
|
|
||||||
pullDistance: distance,
|
|
||||||
canRefresh
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Reset if scrolled or pulling up
|
|
||||||
isDragging.current = false
|
|
||||||
setPullState({
|
|
||||||
isPulling: false,
|
|
||||||
pullDistance: 0,
|
|
||||||
canRefresh: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTouchEnd = async () => {
|
|
||||||
if (!isDragging.current) return
|
|
||||||
|
|
||||||
isDragging.current = false
|
|
||||||
|
|
||||||
if (pullState.canRefresh && !isRefreshing) {
|
|
||||||
// Keep the indicator visible while refreshing
|
|
||||||
setPullState(prev => ({
|
|
||||||
...prev,
|
|
||||||
isPulling: false
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Trigger refresh
|
|
||||||
await onRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset state
|
|
||||||
setPullState({
|
|
||||||
isPulling: false,
|
|
||||||
pullDistance: 0,
|
|
||||||
canRefresh: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTouchCancel = () => {
|
|
||||||
isDragging.current = false
|
|
||||||
setPullState({
|
|
||||||
isPulling: false,
|
|
||||||
pullDistance: 0,
|
|
||||||
canRefresh: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listeners with passive: false to allow preventDefault
|
|
||||||
container.addEventListener('touchstart', handleTouchStart, { passive: true })
|
|
||||||
container.addEventListener('touchmove', handleTouchMove, { passive: false })
|
|
||||||
container.addEventListener('touchend', handleTouchEnd, { passive: true })
|
|
||||||
container.addEventListener('touchcancel', handleTouchCancel, { passive: true })
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
container.removeEventListener('touchstart', handleTouchStart)
|
|
||||||
container.removeEventListener('touchmove', handleTouchMove)
|
|
||||||
container.removeEventListener('touchend', handleTouchEnd)
|
|
||||||
container.removeEventListener('touchcancel', handleTouchCancel)
|
|
||||||
}
|
|
||||||
}, [containerRef, isTouch, disabled, isRefreshing, threshold, resistance, onRefresh, pullState.canRefresh])
|
|
||||||
|
|
||||||
// Reset pull state when refresh completes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isRefreshing && pullState.isPulling) {
|
|
||||||
setPullState({
|
|
||||||
isPulling: false,
|
|
||||||
pullDistance: 0,
|
|
||||||
canRefresh: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [isRefreshing, pullState.isPulling])
|
|
||||||
|
|
||||||
return pullState
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user