mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 08:19:05 +00:00
[Glitch] Update to latest eslint-plugin-react-hooks
Port 9addad8ce5 to glitch-soc
Co-authored-by: diondiondion <mail@diondiondion.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -47,7 +47,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
|
||||
rootClose
|
||||
onHide={handleClose}
|
||||
show={open}
|
||||
target={anchorRef.current}
|
||||
target={anchorRef}
|
||||
placement='top-end'
|
||||
flip
|
||||
offset={offset}
|
||||
|
||||
@@ -76,6 +76,11 @@ export const Carousel = <
|
||||
// Handle slide change
|
||||
const [slideIndex, setSlideIndex] = useState(0);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
// Handle slide heights
|
||||
const [currentSlideHeight, setCurrentSlideHeight] = useState(
|
||||
() => wrapperRef.current?.scrollHeight ?? 0,
|
||||
);
|
||||
const previousSlideHeight = usePrevious(currentSlideHeight);
|
||||
const handleSlideChange = useCallback(
|
||||
(direction: number) => {
|
||||
setSlideIndex((prev) => {
|
||||
@@ -101,16 +106,11 @@ export const Carousel = <
|
||||
[items.length, onChangeSlide],
|
||||
);
|
||||
|
||||
// Handle slide heights
|
||||
const [currentSlideHeight, setCurrentSlideHeight] = useState(
|
||||
wrapperRef.current?.scrollHeight ?? 0,
|
||||
);
|
||||
const previousSlideHeight = usePrevious(currentSlideHeight);
|
||||
const observerRef = useRef<ResizeObserver>(
|
||||
new ResizeObserver(() => {
|
||||
const observerRef = useRef<ResizeObserver | null>(null);
|
||||
observerRef.current ??= new ResizeObserver(() => {
|
||||
handleSlideChange(0);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const wrapperStyles = useSpring({
|
||||
x: `-${slideIndex * 100}%`,
|
||||
height: currentSlideHeight,
|
||||
@@ -200,7 +200,7 @@ export const Carousel = <
|
||||
};
|
||||
|
||||
type CarouselSlideWrapperProps<SlideProps extends CarouselSlideProps> = {
|
||||
observer: ResizeObserver;
|
||||
observer: ResizeObserver | null;
|
||||
className: string;
|
||||
active: boolean;
|
||||
item: SlideProps;
|
||||
@@ -217,7 +217,7 @@ const CarouselSlideWrapper = <SlideProps extends CarouselSlideProps>({
|
||||
}: CarouselSlideWrapperProps<SlideProps>) => {
|
||||
const handleRef = useCallback(
|
||||
(instance: HTMLDivElement | null) => {
|
||||
if (instance) {
|
||||
if (observer && instance) {
|
||||
observer.observe(instance);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
@@ -12,11 +12,15 @@ export const ColumnSearchHeader: React.FC<{
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the component when it turns from active to inactive.
|
||||
// [More on this pattern](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
|
||||
const [previousActive, setPreviousActive] = useState(active);
|
||||
if (active !== previousActive) {
|
||||
setPreviousActive(active);
|
||||
if (!active) {
|
||||
setValue('');
|
||||
}
|
||||
}, [active]);
|
||||
}
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -109,7 +109,7 @@ export const Dropdown: FC<
|
||||
placement='bottom-start'
|
||||
onHide={handleClose}
|
||||
flip
|
||||
target={buttonRef.current}
|
||||
target={buttonRef}
|
||||
popperConfig={{
|
||||
strategy: 'fixed',
|
||||
modifiers: [matchWidth],
|
||||
|
||||
@@ -27,22 +27,23 @@ export const ExitAnimationWrapper: React.FC<{
|
||||
*/
|
||||
children: (delayedIsActive: boolean) => React.ReactNode;
|
||||
}> = ({ isActive = false, delayMs = 500, withEntryDelay, children }) => {
|
||||
const [delayedIsActive, setDelayedIsActive] = useState(false);
|
||||
const [delayedIsActive, setDelayedIsActive] = useState(
|
||||
isActive && !withEntryDelay,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive && !withEntryDelay) {
|
||||
setDelayedIsActive(true);
|
||||
const withDelay = !isActive || withEntryDelay;
|
||||
|
||||
return () => '';
|
||||
} else {
|
||||
const timeout = setTimeout(() => {
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
setDelayedIsActive(isActive);
|
||||
}, delayMs);
|
||||
},
|
||||
withDelay ? delayMs : 0,
|
||||
);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [isActive, delayMs, withEntryDelay]);
|
||||
|
||||
if (!isActive && !delayedIsActive) {
|
||||
|
||||
@@ -27,7 +27,6 @@ export const HoverCardController: React.FC = () => {
|
||||
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
||||
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
|
||||
const [setScrollTimeout] = useTimeout();
|
||||
const location = useLocation();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
cancelEnterTimeout();
|
||||
@@ -36,9 +35,12 @@ export const HoverCardController: React.FC = () => {
|
||||
setAnchor(null);
|
||||
}, [cancelEnterTimeout, cancelLeaveTimeout, setOpen, setAnchor]);
|
||||
|
||||
useEffect(() => {
|
||||
const location = useLocation();
|
||||
const [previousLocation, setPreviousLocation] = useState(location);
|
||||
if (location !== previousLocation) {
|
||||
setPreviousLocation(location);
|
||||
handleClose();
|
||||
}, [handleClose, location]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let isScrolling = false;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, forwardRef } from 'react';
|
||||
import { useCallback, forwardRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -59,23 +59,6 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
||||
},
|
||||
buttonRef,
|
||||
) => {
|
||||
const [activate, setActivate] = useState(false);
|
||||
const [deactivate, setDeactivate] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!animate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activate && !active) {
|
||||
setActivate(false);
|
||||
setDeactivate(true);
|
||||
} else if (!activate && active) {
|
||||
setActivate(true);
|
||||
setDeactivate(false);
|
||||
}
|
||||
}, [setActivate, setDeactivate, animate, active, activate]);
|
||||
|
||||
const handleClick: React.MouseEventHandler<HTMLButtonElement> = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
@@ -116,8 +99,8 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
||||
active,
|
||||
disabled,
|
||||
inverted,
|
||||
activate,
|
||||
deactivate,
|
||||
activate: animate && active,
|
||||
deactivate: animate && !active,
|
||||
overlayed: overlay,
|
||||
'icon-button--with-counter': typeof counter !== 'undefined',
|
||||
});
|
||||
|
||||
@@ -35,6 +35,9 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const isPollExpired = (expiresAt: Model.Poll['expires_at']) =>
|
||||
new Date(expiresAt).getTime() < Date.now();
|
||||
|
||||
interface PollProps {
|
||||
pollId: string;
|
||||
status: Status;
|
||||
@@ -58,8 +61,7 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
|
||||
if (!poll) {
|
||||
return false;
|
||||
}
|
||||
const expiresAt = poll.expires_at;
|
||||
return poll.expired || new Date(expiresAt).getTime() < Date.now();
|
||||
return poll.expired || isPollExpired(poll.expires_at);
|
||||
}, [poll]);
|
||||
const timeRemaining = useMemo(() => {
|
||||
if (!poll) {
|
||||
|
||||
@@ -44,6 +44,7 @@ export const RemoveQuoteHint: React.FC<{
|
||||
|
||||
if (!firstHintId) {
|
||||
firstHintId = uniqueId;
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setIsOnlyHint(true);
|
||||
}
|
||||
|
||||
@@ -64,8 +65,8 @@ export const RemoveQuoteHint: React.FC<{
|
||||
flip
|
||||
offset={[12, 10]}
|
||||
placement='bottom-end'
|
||||
target={anchorRef.current}
|
||||
container={anchorRef.current}
|
||||
target={anchorRef}
|
||||
container={anchorRef}
|
||||
>
|
||||
{({ props, placement }) => (
|
||||
<div
|
||||
|
||||
@@ -86,6 +86,7 @@ export const ScrollContext: React.FC<ScrollContextProps> = ({
|
||||
) =>
|
||||
// Hack to allow accessing scrollBehavior._stateStorage
|
||||
shouldUpdateScroll.call(
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
scrollBehavior,
|
||||
prevLocationContext,
|
||||
locationContext,
|
||||
|
||||
@@ -101,16 +101,17 @@ const Preview: React.FC<{
|
||||
position: FocalPoint;
|
||||
onPositionChange: (arg0: FocalPoint) => void;
|
||||
}> = ({ mediaId, position, onPositionChange }) => {
|
||||
const draggingRef = useRef<boolean>(false);
|
||||
const nodeRef = useRef<HTMLImageElement | HTMLVideoElement | null>(null);
|
||||
|
||||
const [dragging, setDragging] = useState<'started' | 'moving' | null>(null);
|
||||
|
||||
const [x, y] = position;
|
||||
const style = useSpring({
|
||||
to: {
|
||||
left: `${x * 100}%`,
|
||||
top: `${y * 100}%`,
|
||||
},
|
||||
immediate: draggingRef.current,
|
||||
immediate: dragging === 'moving',
|
||||
});
|
||||
const media = useAppSelector((state) =>
|
||||
(
|
||||
@@ -123,8 +124,6 @@ const Preview: React.FC<{
|
||||
me ? state.accounts.get(me) : undefined,
|
||||
);
|
||||
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const setRef = useCallback(
|
||||
(e: HTMLImageElement | HTMLVideoElement | null) => {
|
||||
nodeRef.current = e;
|
||||
@@ -140,20 +139,20 @@ const Preview: React.FC<{
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const { x, y } = getPointerPosition(nodeRef.current, e);
|
||||
draggingRef.current = true; // This will disable the animation for quicker feedback, only do this if the mouse actually moves
|
||||
|
||||
setDragging('moving'); // This will disable the animation for quicker feedback, only do this if the mouse actually moves
|
||||
onPositionChange([x, y]);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setDragging(false);
|
||||
draggingRef.current = false;
|
||||
setDragging(null);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
|
||||
const { x, y } = getPointerPosition(nodeRef.current, e.nativeEvent);
|
||||
|
||||
setDragging(true);
|
||||
setDragging('started');
|
||||
onPositionChange([x, y]);
|
||||
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
@@ -31,15 +31,13 @@ export const AnnualReport: React.FC<{
|
||||
year: string;
|
||||
}> = ({ year }) => {
|
||||
const [response, setResponse] = useState<AnnualReportResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const currentAccount = useAppSelector((state) =>
|
||||
me ? state.accounts.get(me) : undefined,
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
apiRequestGet<AnnualReportResponse>(`v1/annual_reports/${year}`)
|
||||
.then((data) => {
|
||||
dispatch(importFetchedStatuses(data.statuses));
|
||||
|
||||
@@ -55,6 +55,8 @@ const getFrequentlyUsedLanguages = createSelector(
|
||||
.toArray(),
|
||||
);
|
||||
|
||||
const isTextLongEnoughForGuess = (text: string) => text.length > 20;
|
||||
|
||||
const LanguageDropdownMenu: React.FC<{
|
||||
value: string;
|
||||
guess?: string;
|
||||
@@ -375,14 +377,27 @@ export const LanguageDropdown: React.FC = () => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (text.length > 20) {
|
||||
if (isTextLongEnoughForGuess(text)) {
|
||||
debouncedGuess(text, setGuess);
|
||||
} else {
|
||||
debouncedGuess.cancel();
|
||||
setGuess('');
|
||||
}
|
||||
}, [text, setGuess]);
|
||||
|
||||
// Keeping track of the previous render's text length here
|
||||
// to be able to reset the guess when the text length drops
|
||||
// below the threshold needed to make a guess
|
||||
const [wasLongText, setWasLongText] = useState(() =>
|
||||
isTextLongEnoughForGuess(text),
|
||||
);
|
||||
if (wasLongText !== isTextLongEnoughForGuess(text)) {
|
||||
setWasLongText(isTextLongEnoughForGuess(text));
|
||||
|
||||
if (wasLongText) {
|
||||
setGuess('');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={targetRef}>
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState, useRef, useEffect } from 'react';
|
||||
import { useCallback, useState, useRef, useEffect, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
defineMessages,
|
||||
@@ -97,19 +97,29 @@ export const Search: React.FC<{
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState(-1);
|
||||
const [quickActions, setQuickActions] = useState<SearchOption[]>([]);
|
||||
useEffect(() => {
|
||||
setValue(initialValue ?? '');
|
||||
setQuickActions([]);
|
||||
}, [initialValue]);
|
||||
const searchOptions: SearchOption[] = [];
|
||||
|
||||
const unfocus = useCallback(() => {
|
||||
document.querySelector('.ui')?.parentElement?.focus();
|
||||
setExpanded(false);
|
||||
}, []);
|
||||
|
||||
if (searchEnabled) {
|
||||
searchOptions.push(
|
||||
const insertText = useCallback((text: string) => {
|
||||
setValue((currentValue) => {
|
||||
if (currentValue === '') {
|
||||
return text;
|
||||
} else if (currentValue.endsWith(' ')) {
|
||||
return `${currentValue}${text}`;
|
||||
} else {
|
||||
return `${currentValue} ${text}`;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const searchOptions = useMemo(() => {
|
||||
if (!searchEnabled) {
|
||||
return [];
|
||||
} else {
|
||||
const options: SearchOption[] = [
|
||||
{
|
||||
key: 'prompt-has',
|
||||
label: (
|
||||
@@ -131,7 +141,10 @@ export const Search: React.FC<{
|
||||
label: (
|
||||
<>
|
||||
<mark>is:</mark>{' '}
|
||||
<FormattedList type='disjunction' value={['reply', 'sensitive']} />
|
||||
<FormattedList
|
||||
type='disjunction'
|
||||
value={['reply', 'sensitive']}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
action: (e) => {
|
||||
@@ -232,10 +245,14 @@ export const Search: React.FC<{
|
||||
insertText('in:');
|
||||
},
|
||||
},
|
||||
);
|
||||
];
|
||||
return options;
|
||||
}
|
||||
}, [insertText]);
|
||||
|
||||
const recentOptions: SearchOption[] = recent.map((search) => ({
|
||||
const recentOptions: SearchOption[] = useMemo(
|
||||
() =>
|
||||
recent.map((search) => ({
|
||||
key: `${search.type}/${search.q}`,
|
||||
label: labelForRecentSearch(search),
|
||||
action: () => {
|
||||
@@ -248,7 +265,10 @@ export const Search: React.FC<{
|
||||
} else {
|
||||
const queryParams = new URLSearchParams({ q: search.q });
|
||||
if (search.type) queryParams.set('type', search.type);
|
||||
history.push({ pathname: '/search', search: queryParams.toString() });
|
||||
history.push({
|
||||
pathname: '/search',
|
||||
search: queryParams.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
unfocus();
|
||||
@@ -257,23 +277,17 @@ export const Search: React.FC<{
|
||||
e.stopPropagation();
|
||||
void dispatch(forgetSearchResult(search));
|
||||
},
|
||||
}));
|
||||
})),
|
||||
[dispatch, history, recent, unfocus],
|
||||
);
|
||||
|
||||
const navigableOptions = hasValue
|
||||
const navigableOptions: SearchOption[] = useMemo(
|
||||
() =>
|
||||
hasValue
|
||||
? quickActions.concat(searchOptions)
|
||||
: recentOptions.concat(quickActions, searchOptions);
|
||||
|
||||
const insertText = (text: string) => {
|
||||
setValue((currentValue) => {
|
||||
if (currentValue === '') {
|
||||
return text;
|
||||
} else if (currentValue.endsWith(' ')) {
|
||||
return `${currentValue}${text}`;
|
||||
} else {
|
||||
return `${currentValue} ${text}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
: recentOptions.concat(quickActions, searchOptions),
|
||||
[hasValue, quickActions, recentOptions, searchOptions],
|
||||
);
|
||||
|
||||
const submit = useCallback(
|
||||
(q: string, type?: SearchType) => {
|
||||
|
||||
@@ -55,6 +55,11 @@ type ColumnMap = ImmutableMap<'id' | 'uuid' | 'params', string>;
|
||||
const glitchProbability = 1 - 0.0420215528;
|
||||
const totalElefriends = 3;
|
||||
|
||||
const pickRandomFriend = () =>
|
||||
Math.random() < glitchProbability
|
||||
? Math.floor(Math.random() * totalElefriends)
|
||||
: totalElefriends;
|
||||
|
||||
const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -75,11 +80,7 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
||||
false,
|
||||
) as boolean,
|
||||
);
|
||||
const [elefriend, setElefriend] = useState(
|
||||
Math.random() < glitchProbability
|
||||
? Math.floor(Math.random() * totalElefriends)
|
||||
: totalElefriends,
|
||||
);
|
||||
const [elefriend, setElefriend] = useState(pickRandomFriend());
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(mountCompose());
|
||||
|
||||
@@ -19,14 +19,12 @@ const messages = defineMessages({
|
||||
const Blocks: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
||||
const intl = useIntl();
|
||||
const [domains, setDomains] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [next, setNext] = useState<string | undefined>();
|
||||
const hasMore = !!next;
|
||||
const columnRef = useRef<ColumnRef>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
void apiGetDomainBlocks()
|
||||
.then(({ domains, links }) => {
|
||||
const next = links.refs.find((link) => link.rel === 'next');
|
||||
@@ -40,7 +38,7 @@ const Blocks: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [setLoading, setDomains, setNext]);
|
||||
}, []);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -35,12 +35,17 @@ export const Announcement: FC<AnnouncementProps> = ({
|
||||
}, [active, id, dispatch, read]);
|
||||
|
||||
// But visually show the announcement as read only when it goes out of view.
|
||||
const [unread, setUnread] = useState(!read);
|
||||
useEffect(() => {
|
||||
if (!active && unread !== !read) {
|
||||
setUnread(!read);
|
||||
const [isVisuallyRead, setIsVisuallyRead] = useState(read);
|
||||
const [previousActive, setPreviousActive] = useState(active);
|
||||
if (active !== previousActive) {
|
||||
setPreviousActive(active);
|
||||
|
||||
// This marks the announcement as read in the UI only after it
|
||||
// went from active to inactive.
|
||||
if (!active && isVisuallyRead !== read) {
|
||||
setIsVisuallyRead(read);
|
||||
}
|
||||
}
|
||||
}, [active, unread, read]);
|
||||
|
||||
return (
|
||||
<AnimateEmojiProvider>
|
||||
@@ -63,7 +68,7 @@ export const Announcement: FC<AnnouncementProps> = ({
|
||||
|
||||
<ReactionsBar reactions={announcement.reactions} id={announcement.id} />
|
||||
|
||||
{unread && <span className='announcements__unread' />}
|
||||
{!isVisuallyRead && <span className='announcements__unread' />}
|
||||
</AnimateEmojiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -164,12 +164,11 @@ const ListMembers: React.FC<{
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [accountIds, setAccountIds] = useState<string[]>([]);
|
||||
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(!!id);
|
||||
const [mode, setMode] = useState<Mode>('remove');
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
setLoading(true);
|
||||
dispatch(fetchList(id));
|
||||
|
||||
void apiGetAccounts(id)
|
||||
|
||||
@@ -27,17 +27,15 @@ export const ListPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
void dispatch(fetchLists()).then(() => {
|
||||
setLoading(false);
|
||||
|
||||
return '';
|
||||
});
|
||||
}, [dispatch, setLoading]);
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<CollapsiblePanel
|
||||
|
||||
@@ -225,7 +225,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
|
||||
/>
|
||||
|
||||
<div className='explore__search-header'>
|
||||
<Search singleColumn initialValue={trimmedValue} />
|
||||
<Search singleColumn initialValue={trimmedValue} key={trimmedValue} />
|
||||
</div>
|
||||
|
||||
<div className='account__section-headline'>
|
||||
|
||||
@@ -53,8 +53,6 @@ export const DomainBlockModal: React.FC<{
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
apiRequest<DomainBlockPreviewResponse>('GET', 'v1/domain_blocks/preview', {
|
||||
params: { domain },
|
||||
timeout: 5000,
|
||||
@@ -68,7 +66,7 @@ export const DomainBlockModal: React.FC<{
|
||||
setPreview('error');
|
||||
setLoading(false);
|
||||
});
|
||||
}, [setPreview, setLoading, domain]);
|
||||
}, [domain]);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal safety-action-modal' aria-live='polite'>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
@@ -41,40 +41,44 @@ const isHashtagLink = (
|
||||
};
|
||||
|
||||
interface TargetParams {
|
||||
hashtag?: string;
|
||||
accountId?: string;
|
||||
element: HTMLAnchorElement | null;
|
||||
hashtag: string;
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
export const HashtagMenuController: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { signedIn } = useIdentity();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [{ accountId, hashtag }, setTargetParams] = useState<TargetParams>({});
|
||||
const targetRef = useRef<HTMLAnchorElement | null>(null);
|
||||
const location = useLocation();
|
||||
|
||||
const [target, setTarget] = useState<TargetParams | null>(null);
|
||||
const { element = null, accountId, hashtag } = target ?? {};
|
||||
const open = !!element;
|
||||
|
||||
const account = useAppSelector((state) =>
|
||||
accountId ? state.accounts.get(accountId) : undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
targetRef.current = null;
|
||||
}, [setOpen, location]);
|
||||
const location = useLocation();
|
||||
const [previousLocation, setPreviousLocation] = useState(location);
|
||||
if (location !== previousLocation) {
|
||||
setPreviousLocation(location);
|
||||
setTarget(null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
const target = (e.target as HTMLElement).closest('a');
|
||||
const targetElement = (e.target as HTMLElement).closest('a');
|
||||
|
||||
if (e.button !== 0 || e.ctrlKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isHashtagLink(target)) {
|
||||
if (!isHashtagLink(targetElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hashtag = target.text.replace(/^#/, '');
|
||||
const accountId = target.getAttribute('data-menu-hashtag');
|
||||
const hashtag = targetElement.text.replace(/^#/, '');
|
||||
const accountId = targetElement.getAttribute('data-menu-hashtag');
|
||||
|
||||
if (!hashtag || !accountId) {
|
||||
return;
|
||||
@@ -82,9 +86,7 @@ export const HashtagMenuController: React.FC = () => {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
targetRef.current = target;
|
||||
setOpen(true);
|
||||
setTargetParams({ hashtag, accountId });
|
||||
setTarget({ element: targetElement, hashtag, accountId });
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClick, { capture: true });
|
||||
@@ -92,12 +94,11 @@ export const HashtagMenuController: React.FC = () => {
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick);
|
||||
};
|
||||
}, [setTargetParams, setOpen]);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
targetRef.current = null;
|
||||
}, [setOpen]);
|
||||
setTarget(null);
|
||||
}, []);
|
||||
|
||||
const menu = useMemo(() => {
|
||||
const arr: MenuItem[] = [
|
||||
@@ -139,7 +140,7 @@ export const HashtagMenuController: React.FC = () => {
|
||||
offset={offset}
|
||||
placement='bottom'
|
||||
flip
|
||||
target={targetRef}
|
||||
target={element}
|
||||
popperConfig={popperConfig}
|
||||
>
|
||||
{({ props, arrowProps, placement }) => (
|
||||
|
||||
@@ -66,6 +66,7 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
|
||||
_ref,
|
||||
) => {
|
||||
const [index, setIndex] = useState(startIndex);
|
||||
const [zoomedIn, setZoomedIn] = useState(false);
|
||||
const currentMedia = media.get(index);
|
||||
|
||||
const handleChangeIndex = useCallback(
|
||||
@@ -134,7 +135,6 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [zoomedIn, setZoomedIn] = useState(false);
|
||||
const zoomable =
|
||||
currentMedia?.get('type') === 'image' &&
|
||||
((currentMedia.getIn(['meta', 'original', 'width']) as number) >
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
const breakpoints = {
|
||||
narrow: 479, // Device width under which horizontal space is constrained
|
||||
@@ -9,25 +9,20 @@ const breakpoints = {
|
||||
type Breakpoint = keyof typeof breakpoints;
|
||||
|
||||
export const useBreakpoint = (breakpoint: Breakpoint) => {
|
||||
const [isMatching, setIsMatching] = useState(false);
|
||||
const query = `(max-width: ${breakpoints[breakpoint]}px)`;
|
||||
|
||||
useEffect(() => {
|
||||
const mediaWatcher = window.matchMedia(
|
||||
`(max-width: ${breakpoints[breakpoint]}px)`,
|
||||
);
|
||||
const isMatching = useSyncExternalStore(
|
||||
(callback) => {
|
||||
const mediaWatcher = window.matchMedia(query);
|
||||
|
||||
setIsMatching(mediaWatcher.matches);
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
setIsMatching(e.matches);
|
||||
};
|
||||
|
||||
mediaWatcher.addEventListener('change', handleChange);
|
||||
mediaWatcher.addEventListener('change', callback);
|
||||
|
||||
return () => {
|
||||
mediaWatcher.removeEventListener('change', handleChange);
|
||||
mediaWatcher.removeEventListener('change', callback);
|
||||
};
|
||||
}, [breakpoint, setIsMatching]);
|
||||
},
|
||||
() => window.matchMedia(query).matches,
|
||||
);
|
||||
|
||||
return isMatching;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* Returns the previous state of the passed in value.
|
||||
@@ -6,11 +6,21 @@ import { useRef, useEffect } from 'react';
|
||||
*/
|
||||
|
||||
export function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
const [{ previous, current }, setMemory] = useState<{
|
||||
previous: T | undefined;
|
||||
current: T;
|
||||
}>(() => ({ previous: undefined, current: value }));
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
let result = previous;
|
||||
|
||||
return ref.current;
|
||||
if (value !== current) {
|
||||
setMemory({
|
||||
previous: current,
|
||||
current: value,
|
||||
});
|
||||
// Ensure that the returned result updates synchronously
|
||||
result = current;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user