this repo has no description
0
fork

Configure Feed

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

Merge pull request #383 from cheeaun/main

Update from main

authored by

Chee Aun and committed by
GitHub
adf0b351 8aa05422

+268 -149
+26 -8
src/app.css
··· 144 144 user-select: none; 145 145 } 146 146 .deck > header .header-grid { 147 - background-color: var(--bg-blur-color); 147 + background-color: var(--bg-color); 148 + /* background-color: var(--bg-blur-color); 148 149 background-image: linear-gradient(to bottom, var(--bg-color), transparent); 149 - backdrop-filter: saturate(180%) blur(20px); 150 + backdrop-filter: saturate(180%) blur(20px); */ 150 151 border-bottom: var(--hairline-width) solid var(--divider-color); 151 152 min-height: 3em; 152 153 display: grid; ··· 893 894 text-shadow: 0 1px var(--bg-color); 894 895 } 895 896 .status-carousel > ul { 897 + --carousel-gap: 16px; 896 898 display: flex; 897 899 overflow-x: auto; 898 - overflow-y: hidden; 900 + overflow-y: clip; 899 901 scroll-snap-type: x mandatory; 900 902 scroll-behavior: smooth; 901 903 margin: 0; 902 904 padding: 8px 16px; 903 - gap: 16px; 905 + gap: var(--carousel-gap); 904 906 align-items: flex-start; 905 907 counter-reset: index; 906 908 min-height: 160px; 909 + max-height: 65vh; 910 + max-height: 65dvh; 907 911 } 908 912 .status-carousel > ul > li { 909 913 scroll-snap-align: center; ··· 915 919 list-style: none; 916 920 margin: 0; 917 921 padding: 0; 918 - max-height: 65vh; 919 - max-height: 65dvh; 922 + /* max-height: 65vh; 923 + max-height: 65dvh; */ 920 924 counter-increment: index; 921 925 position: relative; 922 926 } 923 927 .status-carousel > ul > li:is(:empty, :has(> a:empty)) { 924 928 display: none; 925 929 } 930 + .status-carousel .status-carousel-beacon { 931 + margin-right: calc(-1 * var(--carousel-gap)); 932 + pointer-events: none; 933 + opacity: 0; 934 + 935 + ~ .status-carousel-beacon { 936 + margin-left: calc(-1 * var(--carousel-gap)); 937 + } 938 + } 926 939 /* 927 940 Assume that browsers that do support inline-size property also support container queries. 928 941 https://www.smashingmagazine.com/2021/05/css-container-queries-use-cases-migration-strategies/#progressive-enhancement-polyfills ··· 941 954 } 942 955 .status-carousel .content-container .content:only-child { 943 956 font-size: calc(100% + 25% * max(2 - var(--content-text-weight), 0)); 957 + 958 + &:has(.status-card) { 959 + font-size: unset; 960 + } 944 961 } 945 962 /* .status-carousel 946 963 .content-container[data-content-text-weight='1'] ··· 1422 1439 right: max(16px, env(safe-area-inset-right)); 1423 1440 padding: 16px; 1424 1441 background-color: var(--button-bg-blur-color); 1425 - backdrop-filter: blur(16px); 1442 + /* backdrop-filter: blur(16px); */ 1426 1443 z-index: 10; 1427 1444 box-shadow: 0 3px 8px -1px var(--drop-shadow-color), 1428 1445 0 10px 36px -4px var(--button-bg-blur-color); ··· 1635 1652 border-radius: 8px; 1636 1653 box-shadow: 0 3px 16px -3px var(--drop-shadow-color); 1637 1654 text-align: left; 1638 - animation: appear-smooth 0.15s ease-in-out; 1655 + /* animation: appear-smooth 0.15s ease-in-out; */ 1639 1656 width: 16em; 1640 1657 max-width: 90vw; 1641 1658 /* overflow: hidden; */ ··· 2471 2488 border-bottom: 0; 2472 2489 border-radius: 16px; 2473 2490 background-color: var(--bg-faded-blur-color); 2491 + backdrop-filter: blur(16px); 2474 2492 background-image: none; 2475 2493 border-radius: 16px; 2476 2494 min-height: 4em;
+47 -10
src/components/icon.jsx
··· 1 + import moize from 'moize'; 1 2 import { useEffect, useRef, useState } from 'preact/hooks'; 2 3 3 4 const SIZES = { ··· 110 111 111 112 const ICONDATA = {}; 112 113 114 + // Memoize the dangerouslySetInnerHTML of the SVGs 115 + const SVGICon = moize( 116 + function ({ size, width, height, body, rotate, flip }) { 117 + return ( 118 + <svg 119 + width={size} 120 + height={size} 121 + viewBox={`0 0 ${width} ${height}`} 122 + dangerouslySetInnerHTML={{ __html: body }} 123 + style={{ 124 + transform: `${rotate ? `rotate(${rotate})` : ''} ${ 125 + flip ? `scaleX(-1)` : '' 126 + }`, 127 + }} 128 + /> 129 + ); 130 + }, 131 + { 132 + isShallowEqual: true, 133 + maxSize: Object.keys(ICONS).length, 134 + }, 135 + ); 136 + 113 137 function Icon({ 114 138 icon, 115 139 size = 'm', ··· 122 146 123 147 const iconSize = SIZES[size]; 124 148 let iconBlock = ICONS[icon]; 149 + if (!iconBlock) { 150 + console.warn(`Icon ${icon} not found`); 151 + return null; 152 + } 153 + 125 154 let rotate, flip; 126 155 if (Array.isArray(iconBlock)) { 127 156 [iconBlock, rotate, flip] = iconBlock; ··· 150 179 }} 151 180 > 152 181 {iconData && ( 153 - <svg 154 - width={iconSize} 155 - height={iconSize} 156 - viewBox={`0 0 ${iconData.width} ${iconData.height}`} 157 - dangerouslySetInnerHTML={{ __html: iconData.body }} 158 - style={{ 159 - transform: `${rotate ? `rotate(${rotate})` : ''} ${ 160 - flip ? `scaleX(-1)` : '' 161 - }`, 162 - }} 182 + // <svg 183 + // width={iconSize} 184 + // height={iconSize} 185 + // viewBox={`0 0 ${iconData.width} ${iconData.height}`} 186 + // dangerouslySetInnerHTML={{ __html: iconData.body }} 187 + // style={{ 188 + // transform: `${rotate ? `rotate(${rotate})` : ''} ${ 189 + // flip ? `scaleX(-1)` : '' 190 + // }`, 191 + // }} 192 + // /> 193 + <SVGICon 194 + size={iconSize} 195 + width={iconData.width} 196 + height={iconData.height} 197 + body={iconData.body} 198 + rotate={rotate} 199 + flip={flip} 163 200 /> 164 201 )} 165 202 </span>
+1 -1
src/components/media-post.css
··· 34 34 word-break: break-word; 35 35 word-wrap: break-word; 36 36 overflow-wrap: break-word; 37 - mix-blend-mode: luminosity; 37 + /* mix-blend-mode: luminosity; */ 38 38 -webkit-line-clamp: 3; 39 39 line-clamp: 3; 40 40 -webkit-box-orient: vertical;
+35 -19
src/components/notification-service.jsx
··· 15 15 import Modal from './modal'; 16 16 import Notification from './notification'; 17 17 18 + { 19 + if ('serviceWorker' in navigator) { 20 + console.log('👂👂👂 Listen to message'); 21 + navigator.serviceWorker.addEventListener('message', (event) => { 22 + console.log('💥💥💥 Message event', event); 23 + const { type, id, accessToken } = event?.data || {}; 24 + if (type === 'notification') { 25 + states.routeNotification = { 26 + id, 27 + accessToken, 28 + }; 29 + } 30 + }); 31 + } 32 + } 33 + 18 34 export default memo(function NotificationService() { 19 35 if (!('serviceWorker' in navigator)) return null; 20 36 ··· 82 98 })(); 83 99 }, [id, accessToken]); 84 100 85 - useLayoutEffect(() => { 86 - // Listen to message from service worker 87 - const handleMessage = (event) => { 88 - console.log('💥💥💥 Message event', event); 89 - const { type, id, accessToken } = event?.data || {}; 90 - if (type === 'notification') { 91 - states.routeNotification = { 92 - id, 93 - accessToken, 94 - }; 95 - } 96 - }; 97 - console.log('👂👂👂 Listen to message'); 98 - navigator.serviceWorker.addEventListener('message', handleMessage); 99 - return () => { 100 - console.log('👂👂👂 Remove listen to message'); 101 - navigator.serviceWorker.removeEventListener('message', handleMessage); 102 - }; 103 - }, []); 101 + // useLayoutEffect(() => { 102 + // // Listen to message from service worker 103 + // const handleMessage = (event) => { 104 + // console.log('💥💥💥 Message event', event); 105 + // const { type, id, accessToken } = event?.data || {}; 106 + // if (type === 'notification') { 107 + // states.routeNotification = { 108 + // id, 109 + // accessToken, 110 + // }; 111 + // } 112 + // }; 113 + // console.log('👂👂👂 Listen to message'); 114 + // navigator.serviceWorker.addEventListener('message', handleMessage); 115 + // return () => { 116 + // console.log('👂👂👂 Remove listen to message'); 117 + // navigator.serviceWorker.removeEventListener('message', handleMessage); 118 + // }; 119 + // }, []); 104 120 105 121 useLayoutEffect(() => { 106 122 if (navigator?.clearAppBadge) {
+3 -1
src/components/shortcuts-settings.jsx
··· 172 172 id: 'search', 173 173 title: ({ query }) => (query ? `"${query}"` : 'Search'), 174 174 path: ({ query }) => 175 - query ? `/search?q=${query}&type=statuses` : '/search', 175 + query 176 + ? `/search?q=${encodeURIComponent(query)}&type=statuses` 177 + : '/search', 176 178 icon: 'search', 177 179 excludeViewMode: ({ query }) => (!query ? ['multi-column'] : []), 178 180 },
+4 -2
src/components/shortcuts.css
··· 59 59 left: 0; 60 60 right: 0; 61 61 z-index: 100; 62 - background-color: var(--bg-blur-color); 63 - backdrop-filter: blur(16px) saturate(3); 62 + background-color: var(--bg-color); 63 + /* background-color: var(--bg-blur-color); 64 + backdrop-filter: blur(16px) saturate(3); */ 64 65 border-top: var(--hairline-width) solid var(--outline-color); 65 66 box-shadow: 0 -8px 16px -8px var(--drop-shadow-color); 66 67 overflow: auto; ··· 165 166 padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 166 167 env(safe-area-inset-left); 167 168 background-color: var(--bg-faded-blur-color); 169 + backdrop-filter: blur(16px); 168 170 border: 0; 169 171 box-shadow: none; 170 172 border-bottom: var(--hairline-width) solid var(--bg-faded-color);
+12 -4
src/components/status.css
··· 206 206 .status-card:not(.status-carousel .status) 207 207 :is(.content, .poll, .media-container) { 208 208 max-height: 160px !important; 209 - overflow: hidden; 209 + overflow: clip; 210 210 } 211 - .status.small:not(.status-carousel .status) 211 + .status.small:not(.status-carousel .status, .status.large .status) 212 212 .status-card 213 - :is(.content, .poll, .media-container) { 213 + :is(.content, .poll, .media-container:not(.media-gt2)) { 214 214 max-height: 80px !important; 215 215 } 216 216 .status.large .status-card :is(.content, .poll, .media-container) { ··· 730 730 tab-size: 2; 731 731 text-wrap: pretty; 732 732 } 733 + .status-card .content p { 734 + margin-block: min(0.25em, 4px); 735 + } 733 736 .status .content p:first-child { 734 737 margin-block-start: 0; 735 738 } ··· 958 961 width: min(var(--width), 100%); 959 962 height: auto; 960 963 max-height: 60vh; 964 + } 965 + .status.status-card .media-container.media-eq1 .media { 966 + max-height: 160px; 967 + width: auto; 968 + max-width: min(var(--width), 100%); 961 969 } 962 970 /* Special media borders */ 963 971 .status .media-container.media-eq2 .media:first-of-type { ··· 1957 1965 color: var(--media-fg-color); 1958 1966 background-color: var(--media-bg-color); 1959 1967 border: var(--hairline-width) solid var(--media-outline-color); 1960 - mix-blend-mode: luminosity; 1968 + /* mix-blend-mode: luminosity; */ 1961 1969 border-radius: 4px; 1962 1970 padding: 4px; 1963 1971 opacity: 0.65;
+41 -15
src/components/timeline.jsx
··· 239 239 setNearReachStart(nearReachStart); 240 240 if (reachStart) { 241 241 loadItems(true); 242 - } else if (nearReachEnd || (reachEnd && showMore)) { 243 - loadItems(); 244 242 } 243 + // else if (nearReachEnd || (reachEnd && showMore)) { 244 + // loadItems(); 245 + // } 245 246 }, 246 247 [], 247 248 ); ··· 451 452 {uiState === 'default' && 452 453 (showMore ? ( 453 454 <InView 455 + root={scrollableRef.current} 456 + rootMargin={`0px 0px ${screen.height * 1.5}px 0px`} 454 457 onChange={(inView) => { 455 458 if (inView) { 456 459 loadItems(); ··· 695 698 // }); 696 699 const startButtonRef = useRef(); 697 700 const endButtonRef = useRef(); 698 - useScrollFn( 699 - { 700 - scrollableRef: carouselRef, 701 - direction: 'horizontal', 702 - init: true, 703 - }, 704 - ({ reachStart, reachEnd }) => { 705 - if (startButtonRef.current) startButtonRef.current.disabled = reachStart; 706 - if (endButtonRef.current) endButtonRef.current.disabled = reachEnd; 707 - }, 708 - [], 709 - ); 701 + // useScrollFn( 702 + // { 703 + // scrollableRef: carouselRef, 704 + // direction: 'horizontal', 705 + // init: true, 706 + // }, 707 + // ({ reachStart, reachEnd }) => { 708 + // if (startButtonRef.current) startButtonRef.current.disabled = reachStart; 709 + // if (endButtonRef.current) endButtonRef.current.disabled = reachEnd; 710 + // }, 711 + // [], 712 + // ); 710 713 // useEffect(() => { 711 714 // init?.(); 712 715 // }, []); 716 + 717 + const [render, setRender] = useState(false); 718 + useEffect(() => { 719 + setTimeout(() => { 720 + setRender(true); 721 + }, 1); 722 + }, []); 713 723 714 724 return ( 715 725 <div class={`status-carousel ${className}`}> ··· 746 756 </button> 747 757 </span> 748 758 </header> 749 - <ul ref={carouselRef}>{children}</ul> 759 + <ul ref={carouselRef}> 760 + <InView 761 + class="status-carousel-beacon" 762 + onChange={(inView) => { 763 + if (startButtonRef.current) 764 + startButtonRef.current.disabled = inView; 765 + }} 766 + /> 767 + {children[0]} 768 + {render && children.slice(1)} 769 + <InView 770 + class="status-carousel-beacon" 771 + onChange={(inView) => { 772 + if (endButtonRef.current) endButtonRef.current.disabled = inView; 773 + }} 774 + /> 775 + </ul> 750 776 </div> 751 777 ); 752 778 }
+1 -4
src/pages/search.jsx
··· 17 17 import { fetchRelationships } from '../utils/relationships'; 18 18 import shortenNumber from '../utils/shorten-number'; 19 19 import usePageVisibility from '../utils/usePageVisibility'; 20 - import useScroll from '../utils/useScroll'; 21 20 import useTitle from '../utils/useTitle'; 22 21 23 22 const SHORT_LIMIT = 5; ··· 151 150 })(); 152 151 } 153 152 154 - const { reachStart } = useScroll({ 155 - scrollableRef, 156 - }); 157 153 const lastHiddenTime = useRef(); 158 154 usePageVisibility((visible) => { 155 + const reachStart = scrollableRef.current?.scrollTop === 0; 159 156 if (visible && reachStart) { 160 157 const timeDiff = Date.now() - lastHiddenTime.current; 161 158 if (!lastHiddenTime.current || timeDiff > 1000 * 3) {
+1 -4
src/pages/status.css
··· 7 7 text-overflow: ellipsis; 8 8 overflow: hidden; 9 9 white-space: nowrap; 10 + align-self: stretch; 10 11 } 11 12 .status-deck header h1 .deck-back { 12 13 margin-left: -16px; 13 - } 14 - 15 - .status-deck header.inview h1 { 16 - font-weight: bold; 17 14 } 18 15 19 16 .hero-heading {
+36 -20
src/pages/status.jsx
··· 545 545 const ancestors = statuses.filter((s) => s.ancestor); 546 546 547 547 const [heroInView, setHeroInView] = useState(true); 548 - const onView = useDebouncedCallback(setHeroInView, 100); 549 548 const heroPointer = useMemo(() => { 550 549 // get top offset of heroStatus 551 550 if (!heroStatusRef.current || heroInView) return null; ··· 652 651 } 653 652 }); 654 653 655 - const { nearReachStart } = useScroll({ 656 - scrollableRef, 657 - distanceFromStartPx: 16, 658 - }); 654 + const [reachTopPost, setReachTopPost] = useState(false); 655 + // const { nearReachStart } = useScroll({ 656 + // scrollableRef, 657 + // distanceFromStartPx: 16, 658 + // }); 659 659 660 660 const initialPageState = useRef(showMedia ? 'media+status' : 'status'); 661 661 ··· 693 693 }, [mediaStatusID, showMedia]); 694 694 695 695 const renderStatus = useCallback( 696 - (status) => { 696 + (status, i) => { 697 697 const { 698 698 id: statusID, 699 699 ancestor, ··· 735 735 <> 736 736 <InView 737 737 threshold={0.1} 738 - onChange={onView} 738 + onChange={(inView) => { 739 + queueMicrotask(() => { 740 + requestAnimationFrame(() => { 741 + setHeroInView(inView); 742 + }); 743 + }); 744 + }} 739 745 class="status-focus" 740 746 tabIndex={0} 741 747 > ··· 810 816 resetScrollPosition(statusID); 811 817 }} 812 818 > 813 - <Status 814 - statusID={statusID} 815 - instance={instance} 816 - withinContext 817 - size={thread || ancestor ? 'm' : 's'} 818 - enableTranslate 819 - onMediaClick={handleMediaClick} 820 - onStatusLinkClick={handleStatusLinkClick} 821 - /> 819 + <InView 820 + skip={i !== 0 || !ancestor} 821 + threshold={0.5} 822 + onChange={(inView) => { 823 + queueMicrotask(() => { 824 + requestAnimationFrame(() => { 825 + setReachTopPost(inView); 826 + }); 827 + }); 828 + }} 829 + > 830 + <Status 831 + statusID={statusID} 832 + instance={instance} 833 + withinContext 834 + size={thread || ancestor ? 'm' : 's'} 835 + enableTranslate 836 + onMediaClick={handleMediaClick} 837 + onStatusLinkClick={handleStatusLinkClick} 838 + /> 839 + </InView> 822 840 {ancestor && repliesCount > 1 && ( 823 841 <div class="replies-link"> 824 842 <Icon icon="comment2" />{' '} ··· 935 953 }} 936 954 > 937 955 <header 938 - class={`${heroInView ? 'inview' : ''} ${ 939 - uiState === 'loading' ? 'loading' : '' 940 - }`} 956 + class={`${uiState === 'loading' ? 'loading' : ''}`} 941 957 onDblClick={(e) => { 942 958 // reload statuses 943 959 states.reloadStatusPage++; ··· 1011 1027 behavior: 'smooth', 1012 1028 }); 1013 1029 }} 1014 - hidden={!ancestors.length || nearReachStart} 1030 + hidden={!ancestors.length || reachTopPost} 1015 1031 title={`${ancestors.length} posts above ‒ Go to top`} 1016 1032 > 1017 1033 <Icon icon="arrow-up" />
+1 -1
src/utils/get-instance-status-url.js
··· 22 22 }; 23 23 } 24 24 } 25 - return null; 25 + return {}; 26 26 } 27 27 28 28 function getInstanceStatusURL(url) {
+4 -9
src/utils/states.js
··· 192 192 // THREAD TRAVERSER 193 193 if (!skipThreading) { 194 194 queueMicrotask(() => { 195 - threadifyStatus(status, instance); 196 - if (status.reblog) { 197 - queueMicrotask(() => { 198 - threadifyStatus(status.reblog, instance); 199 - }); 200 - } 195 + threadifyStatus(status.reblog || status, instance); 201 196 }); 202 197 } 203 198 204 199 // UNFURLER 205 200 if (!skipUnfurling) { 206 201 queueMicrotask(() => { 207 - unfurlStatus(status, instance); 202 + unfurlStatus(status.reblog || status, instance); 208 203 }); 209 204 } 210 205 } ··· 253 248 const fauxDiv = document.createElement('div'); 254 249 export function unfurlStatus(status, instance) { 255 250 const { instance: currentInstance } = api(); 256 - const content = status.reblog?.content || status.content; 251 + const content = status?.content; 257 252 const hasLink = /<a/i.test(content); 258 253 if (hasLink) { 259 - const sKey = statusKey(status?.reblog?.id || status?.id, instance); 254 + const sKey = statusKey(status?.id, instance); 260 255 fauxDiv.innerHTML = content; 261 256 const links = fauxDiv.querySelectorAll( 262 257 'a[href]:not(.u-url):not(.mention):not(.hashtag)',
+53 -49
src/utils/useScrollFn.js
··· 1 - import { useEffect, useLayoutEffect, useState } from 'preact/hooks'; 1 + import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks'; 2 + import { useThrottledCallback } from 'use-debounce'; 2 3 3 4 export default function useScrollFn( 4 5 { ··· 22 23 const [nearReachStart, setNearReachStart] = useState(false); 23 24 const [nearReachEnd, setNearReachEnd] = useState(false); 24 25 const isVertical = direction === 'vertical'; 26 + const previousScrollStart = useRef(null); 25 27 26 - useLayoutEffect(() => { 28 + const onScroll = useThrottledCallback(() => { 27 29 const scrollableElement = scrollableRef.current; 28 - if (!scrollableElement) return {}; 29 - let previousScrollStart = isVertical 30 - ? scrollableElement.scrollTop 31 - : scrollableElement.scrollLeft; 32 - 33 - function onScroll() { 34 - const { 35 - scrollTop, 36 - scrollLeft, 37 - scrollHeight, 38 - scrollWidth, 39 - clientHeight, 40 - clientWidth, 41 - } = scrollableElement; 42 - const scrollStart = isVertical ? scrollTop : scrollLeft; 43 - const scrollDimension = isVertical ? scrollHeight : scrollWidth; 44 - const clientDimension = isVertical ? clientHeight : clientWidth; 45 - const scrollDistance = Math.abs(scrollStart - previousScrollStart); 46 - const distanceFromStartPx = 47 - _distanceFromStartPx || 48 - Math.min( 49 - clientDimension * distanceFromStart, 50 - scrollDimension, 51 - scrollStart, 52 - ); 53 - const distanceFromEndPx = 54 - _distanceFromEndPx || 55 - Math.min( 56 - clientDimension * distanceFromEnd, 57 - scrollDimension, 58 - scrollDimension - scrollStart - clientDimension, 59 - ); 60 - 61 - if ( 62 - scrollDistance >= 63 - (previousScrollStart < scrollStart 64 - ? scrollThresholdEnd 65 - : scrollThresholdStart) 66 - ) { 67 - setScrollDirection(previousScrollStart < scrollStart ? 'end' : 'start'); 68 - previousScrollStart = scrollStart; 69 - } 30 + const { 31 + scrollTop, 32 + scrollLeft, 33 + scrollHeight, 34 + scrollWidth, 35 + clientHeight, 36 + clientWidth, 37 + } = scrollableElement; 38 + const scrollStart = isVertical ? scrollTop : scrollLeft; 39 + const scrollDimension = isVertical ? scrollHeight : scrollWidth; 40 + const clientDimension = isVertical ? clientHeight : clientWidth; 41 + const scrollDistance = Math.abs(scrollStart - previousScrollStart.current); 42 + const distanceFromStartPx = 43 + _distanceFromStartPx || 44 + Math.min( 45 + clientDimension * distanceFromStart, 46 + scrollDimension, 47 + scrollStart, 48 + ); 49 + const distanceFromEndPx = 50 + _distanceFromEndPx || 51 + Math.min( 52 + clientDimension * distanceFromEnd, 53 + scrollDimension, 54 + scrollDimension - scrollStart - clientDimension, 55 + ); 70 56 71 - setReachStart(scrollStart <= 0); 72 - setReachEnd(scrollStart + clientDimension >= scrollDimension); 73 - setNearReachStart(scrollStart <= distanceFromStartPx); 74 - setNearReachEnd( 75 - scrollStart + clientDimension >= scrollDimension - distanceFromEndPx, 57 + if ( 58 + scrollDistance >= 59 + (previousScrollStart.current < scrollStart 60 + ? scrollThresholdEnd 61 + : scrollThresholdStart) 62 + ) { 63 + setScrollDirection( 64 + previousScrollStart.current < scrollStart ? 'end' : 'start', 76 65 ); 66 + previousScrollStart.current = scrollStart; 77 67 } 68 + 69 + setReachStart(scrollStart <= 0); 70 + setReachEnd(scrollStart + clientDimension >= scrollDimension); 71 + setNearReachStart(scrollStart <= distanceFromStartPx); 72 + setNearReachEnd( 73 + scrollStart + clientDimension >= scrollDimension - distanceFromEndPx, 74 + ); 75 + }, 500); 76 + 77 + useLayoutEffect(() => { 78 + const scrollableElement = scrollableRef.current; 79 + if (!scrollableElement) return {}; 80 + previousScrollStart.current = 81 + scrollableElement[isVertical ? 'scrollTop' : 'scrollLeft']; 78 82 79 83 scrollableElement.addEventListener('scroll', onScroll, { passive: true }); 80 84
+3 -2
vite.config.js
··· 9 9 import { VitePWA } from 'vite-plugin-pwa'; 10 10 import removeConsole from 'vite-plugin-remove-console'; 11 11 12 + const allowedEnvPrefixes = ['VITE_', 'PHANPY_']; 12 13 const { NODE_ENV } = process.env; 13 14 const { 14 15 PHANPY_CLIENT_NAME: CLIENT_NAME, 15 16 PHANPY_APP_ERROR_LOGGING: ERROR_LOGGING, 16 - } = loadEnv('production', process.cwd()); 17 + } = loadEnv('production', process.cwd(), allowedEnvPrefixes); 17 18 18 19 const now = new Date(); 19 20 let commitHash; ··· 35 36 // https://vitejs.dev/config/ 36 37 export default defineConfig({ 37 38 base: './', 38 - envPrefix: ['VITE_', 'PHANPY_'], 39 + envPrefix: allowedEnvPrefixes, 39 40 mode: NODE_ENV, 40 41 define: { 41 42 __BUILD_TIME__: JSON.stringify(now),