mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-15 08:48:53 +00:00
[Glitch] Replace react-router-scroll-4 with inlined implementation
Port d801cf8e59 to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
|
import type { useLocation } from 'react-router';
|
||||||
import { Router as OriginalRouter, useHistory } from 'react-router';
|
import { Router as OriginalRouter, useHistory } from 'react-router';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -18,7 +19,9 @@ interface MastodonLocationState {
|
|||||||
mastodonModalKey?: string;
|
mastodonModalKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocationState = MastodonLocationState | null | undefined;
|
export type LocationState = MastodonLocationState | null | undefined;
|
||||||
|
|
||||||
|
export type MastodonLocation = ReturnType<typeof useLocation<LocationState>>;
|
||||||
|
|
||||||
type HistoryPath = Path | LocationDescriptor<LocationState>;
|
type HistoryPath = Path | LocationDescriptor<LocationState>;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { connect } from 'react-redux';
|
|||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
import { ScrollContainer } from 'flavours/glitch/containers/scroll_container';
|
||||||
|
|
||||||
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
||||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
|
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Route } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Provider as ReduxProvider } from 'react-redux';
|
import { Provider as ReduxProvider } from 'react-redux';
|
||||||
|
|
||||||
import { ScrollContext } from 'react-router-scroll-4';
|
|
||||||
|
|
||||||
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
|
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
|
||||||
import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings';
|
import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings';
|
||||||
@@ -21,6 +20,8 @@ import { store } from 'flavours/glitch/store';
|
|||||||
import { isProduction } from 'flavours/glitch/utils/environment';
|
import { isProduction } from 'flavours/glitch/utils/environment';
|
||||||
import { BodyScrollLock } from 'flavours/glitch/features/ui/components/body_scroll_lock';
|
import { BodyScrollLock } from 'flavours/glitch/features/ui/components/body_scroll_lock';
|
||||||
|
|
||||||
|
import { ScrollContext } from './scroll_container/scroll_context';
|
||||||
|
|
||||||
const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`;
|
const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`;
|
||||||
|
|
||||||
const hydrateAction = hydrateStore(initialState);
|
const hydrateAction = hydrateStore(initialState);
|
||||||
@@ -50,10 +51,6 @@ export default class Mastodon extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldUpdateScroll (prevRouterProps, { location }) {
|
|
||||||
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<IdentityContext.Provider value={this.identity}>
|
<IdentityContext.Provider value={this.identity}>
|
||||||
@@ -61,7 +58,7 @@ export default class Mastodon extends PureComponent {
|
|||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Router>
|
<Router>
|
||||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
<ScrollContext>
|
||||||
<Route path='/' component={UI} />
|
<Route path='/' component={UI} />
|
||||||
</ScrollContext>
|
</ScrollContext>
|
||||||
<BodyScrollLock />
|
<BodyScrollLock />
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { ScrollContainer as OriginalScrollContainer } from 'react-router-scroll-4';
|
|
||||||
|
|
||||||
// ScrollContainer is used to automatically scroll to the top when pushing a
|
|
||||||
// new history state and remembering the scroll position when going back.
|
|
||||||
// There are a few things we need to do differently, though.
|
|
||||||
const defaultShouldUpdateScroll = (prevRouterProps, { location }) => {
|
|
||||||
// If the change is caused by opening a modal, do not scroll to top
|
|
||||||
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default
|
|
||||||
class ScrollContainer extends OriginalScrollContainer {
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
shouldUpdateScroll: defaultShouldUpdateScroll,
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { MastodonLocation } from 'flavours/glitch/components/router';
|
||||||
|
|
||||||
|
export type ShouldUpdateScrollFn = (
|
||||||
|
prevLocationContext: MastodonLocation | null,
|
||||||
|
locationContext: MastodonLocation,
|
||||||
|
) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScrollBehavior will automatically scroll to the top on navigations
|
||||||
|
* or restore saved scroll positions, but on some location changes we
|
||||||
|
* need to prevent this.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const defaultShouldUpdateScroll: ShouldUpdateScrollFn = (
|
||||||
|
prevLocation,
|
||||||
|
location,
|
||||||
|
) => {
|
||||||
|
// If the change is caused by opening a modal, do not scroll to top
|
||||||
|
const shouldUpdateScroll = !(
|
||||||
|
location.state?.mastodonModalKey &&
|
||||||
|
location.state.mastodonModalKey !== prevLocation?.state?.mastodonModalKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return shouldUpdateScroll;
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { useContext, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { defaultShouldUpdateScroll } from './default_should_update_scroll';
|
||||||
|
import type { ShouldUpdateScrollFn } from './default_should_update_scroll';
|
||||||
|
import { ScrollBehaviorContext } from './scroll_context';
|
||||||
|
|
||||||
|
interface ScrollContainerProps {
|
||||||
|
/**
|
||||||
|
* This key must be static for the element & not change
|
||||||
|
* while the component is mounted.
|
||||||
|
*/
|
||||||
|
scrollKey: string;
|
||||||
|
shouldUpdateScroll?: ShouldUpdateScrollFn;
|
||||||
|
children: React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `ScrollContainer` is used to manage the scroll position of elements on the page
|
||||||
|
* that can be scrolled independently of the page body.
|
||||||
|
* This component is a port of the unmaintained https://github.com/ytase/react-router-scroll/
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ScrollContainer: React.FC<ScrollContainerProps> = ({
|
||||||
|
children,
|
||||||
|
scrollKey,
|
||||||
|
shouldUpdateScroll = defaultShouldUpdateScroll,
|
||||||
|
}) => {
|
||||||
|
const scrollBehaviorContext = useContext(ScrollBehaviorContext);
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLElement>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register/unregister scrollable element with ScrollBehavior
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scrollBehaviorContext || !containerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollBehaviorContext.registerElement(
|
||||||
|
scrollKey,
|
||||||
|
containerRef.current,
|
||||||
|
(prevLocation, location) => {
|
||||||
|
// Hack to allow accessing scrollBehavior._stateStorage
|
||||||
|
return shouldUpdateScroll.call(
|
||||||
|
scrollBehaviorContext.scrollBehavior,
|
||||||
|
prevLocation,
|
||||||
|
location,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scrollBehaviorContext.unregisterElement(scrollKey);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return React.Children.only(
|
||||||
|
React.cloneElement(children, { ref: containerRef }),
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { useLocation, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { LocationBase } from 'scroll-behavior';
|
||||||
|
import ScrollBehavior from 'scroll-behavior';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
LocationState,
|
||||||
|
MastodonLocation,
|
||||||
|
} from 'flavours/glitch/components/router';
|
||||||
|
import { usePrevious } from 'flavours/glitch/hooks/usePrevious';
|
||||||
|
|
||||||
|
import { defaultShouldUpdateScroll } from './default_should_update_scroll';
|
||||||
|
import type { ShouldUpdateScrollFn } from './default_should_update_scroll';
|
||||||
|
import { SessionStorage } from './state_storage';
|
||||||
|
|
||||||
|
type ScrollBehaviorInstance = InstanceType<
|
||||||
|
typeof ScrollBehavior<LocationBase, MastodonLocation>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface ScrollBehaviorContextType {
|
||||||
|
registerElement: (
|
||||||
|
key: string,
|
||||||
|
element: HTMLElement,
|
||||||
|
shouldUpdateScroll: (
|
||||||
|
prevLocationContext: MastodonLocation | null,
|
||||||
|
locationContext: MastodonLocation,
|
||||||
|
) => boolean,
|
||||||
|
) => void;
|
||||||
|
unregisterElement: (key: string) => void;
|
||||||
|
scrollBehavior?: ScrollBehaviorInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScrollBehaviorContext =
|
||||||
|
React.createContext<ScrollBehaviorContextType | null>(null);
|
||||||
|
|
||||||
|
interface ScrollContextProps {
|
||||||
|
shouldUpdateScroll?: ShouldUpdateScrollFn;
|
||||||
|
children: React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A top-level wrapper that provides the app with an instance of the
|
||||||
|
* ScrollBehavior object. scroll-behavior is a library for managing the
|
||||||
|
* scroll position of a single-page app in the same way the browser would
|
||||||
|
* normally do for a multi-page app. This means it'll scroll back to top
|
||||||
|
* when navigating to a new page, and will restore the scroll position
|
||||||
|
* when navigating e.g. using `history.back`.
|
||||||
|
* The library keeps a record of scroll positions in session storage.
|
||||||
|
*
|
||||||
|
* This component is a port of the unmaintained https://github.com/ytase/react-router-scroll/
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ScrollContext: React.FC<ScrollContextProps> = ({
|
||||||
|
children,
|
||||||
|
shouldUpdateScroll = defaultShouldUpdateScroll,
|
||||||
|
}) => {
|
||||||
|
const location = useLocation<LocationState>();
|
||||||
|
const history = useHistory<LocationState>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep the current location in a mutable ref so that ScrollBehavior's
|
||||||
|
* `getCurrentLocation` can access it without having to recreate the
|
||||||
|
* whole ScrollBehavior object
|
||||||
|
*/
|
||||||
|
const currentLocationRef = useRef(location);
|
||||||
|
useEffect(() => {
|
||||||
|
currentLocationRef.current = location;
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise ScrollBehavior object once – using state rather
|
||||||
|
* than a ref to simplify the types and ensure it's defined immediately.
|
||||||
|
*/
|
||||||
|
const [scrollBehavior] = useState(
|
||||||
|
(): ScrollBehaviorInstance =>
|
||||||
|
new ScrollBehavior({
|
||||||
|
addNavigationListener: history.listen.bind(history),
|
||||||
|
stateStorage: new SessionStorage(),
|
||||||
|
getCurrentLocation: () =>
|
||||||
|
currentLocationRef.current as unknown as LocationBase,
|
||||||
|
shouldUpdateScroll: (
|
||||||
|
prevLocationContext: MastodonLocation | null,
|
||||||
|
locationContext: MastodonLocation,
|
||||||
|
) =>
|
||||||
|
// Hack to allow accessing scrollBehavior._stateStorage
|
||||||
|
shouldUpdateScroll.call(
|
||||||
|
scrollBehavior,
|
||||||
|
prevLocationContext,
|
||||||
|
locationContext,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle scroll update when location changes
|
||||||
|
*/
|
||||||
|
const prevLocation = usePrevious(location) ?? null;
|
||||||
|
useEffect(() => {
|
||||||
|
scrollBehavior.updateScroll(prevLocation, location);
|
||||||
|
}, [location, prevLocation, scrollBehavior]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop Scrollbehavior on unmount
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
scrollBehavior.stop();
|
||||||
|
};
|
||||||
|
}, [scrollBehavior]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide the app with a way to register separately scrollable
|
||||||
|
* elements to also be tracked by ScrollBehavior. (By default
|
||||||
|
* ScrollBehavior only handles scrolling on the main document body.)
|
||||||
|
*/
|
||||||
|
const contextValue = useMemo<ScrollBehaviorContextType>(
|
||||||
|
() => ({
|
||||||
|
registerElement: (key, element, shouldUpdateScroll) => {
|
||||||
|
scrollBehavior.registerElement(
|
||||||
|
key,
|
||||||
|
element,
|
||||||
|
shouldUpdateScroll,
|
||||||
|
location,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
unregisterElement: (key) => {
|
||||||
|
scrollBehavior.unregisterElement(key);
|
||||||
|
},
|
||||||
|
scrollBehavior,
|
||||||
|
}),
|
||||||
|
[location, scrollBehavior],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollBehaviorContext.Provider value={contextValue}>
|
||||||
|
{React.Children.only(children)}
|
||||||
|
</ScrollBehaviorContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type { LocationBase, ScrollPosition } from 'scroll-behavior';
|
||||||
|
|
||||||
|
const STATE_KEY_PREFIX = '@@scroll|';
|
||||||
|
|
||||||
|
interface LocationBaseWithKey extends LocationBase {
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module is part of our port of https://github.com/ytase/react-router-scroll/
|
||||||
|
* and handles storing scroll positions in SessionStorage.
|
||||||
|
* Stored positions (`[x, y]`) are keyed by the location key and an optional
|
||||||
|
* `scrollKey` that's used for to track separately scrollable elements other
|
||||||
|
* than the document body.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SessionStorage {
|
||||||
|
read(
|
||||||
|
location: LocationBaseWithKey,
|
||||||
|
key: string | null,
|
||||||
|
): ScrollPosition | null {
|
||||||
|
const stateKey = this.getStateKey(location, key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = sessionStorage.getItem(stateKey);
|
||||||
|
return value ? (JSON.parse(value) as ScrollPosition) : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save(location: LocationBaseWithKey, key: string | null, value: unknown) {
|
||||||
|
const stateKey = this.getStateKey(location, key);
|
||||||
|
const storedValue = JSON.stringify(value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(stateKey, storedValue);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStateKey(location: LocationBaseWithKey, key: string | null) {
|
||||||
|
const locationKey = location.key;
|
||||||
|
const stateKeyBase = `${STATE_KEY_PREFIX}${locationKey}`;
|
||||||
|
return key == null ? stateKeyBase : `${stateKeyBase}|${key}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
|||||||
import { LoadMore } from 'flavours/glitch/components/load_more';
|
import { LoadMore } from 'flavours/glitch/components/load_more';
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
import { RadioButton } from 'flavours/glitch/components/radio_button';
|
import { RadioButton } from 'flavours/glitch/components/radio_button';
|
||||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
import { ScrollContainer } from 'flavours/glitch/containers/scroll_container';
|
||||||
import { useSearchParam } from 'flavours/glitch/hooks/useSearchParam';
|
import { useSearchParam } from 'flavours/glitch/hooks/useSearchParam';
|
||||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
@@ -209,7 +209,6 @@ export const Directory: React.FC<{
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{multiColumn && !pinned ? (
|
{multiColumn && !pinned ? (
|
||||||
// @ts-expect-error ScrollContainer is not properly typed yet
|
|
||||||
<ScrollContainer scrollKey='directory'>
|
<ScrollContainer scrollKey='directory'>
|
||||||
{scrollableArea}
|
{scrollableArea}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac
|
|||||||
import { Hotkeys } from 'flavours/glitch/components/hotkeys';
|
import { Hotkeys } from 'flavours/glitch/components/hotkeys';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
import { ScrollContainer } from 'flavours/glitch/containers/scroll_container';
|
||||||
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
||||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||||
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
|
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
|
||||||
@@ -551,9 +551,9 @@ class Status extends ImmutablePureComponent {
|
|||||||
this.setState({ fullscreen: isFullscreen() });
|
this.setState({ fullscreen: isFullscreen() });
|
||||||
};
|
};
|
||||||
|
|
||||||
shouldUpdateScroll = (prevRouterProps, { location }) => {
|
shouldUpdateScroll = (prevLocation, location) => {
|
||||||
// Do not change scroll when opening a modal
|
// Do not change scroll when opening a modal
|
||||||
if (location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey) {
|
if (location.state?.mastodonModalKey !== prevLocation?.state?.mastodonModalKey) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user