0
0
Fork 0

Move app/javascript/hooks to app/javascript/mastodon/hooks (#34077)

This commit is contained in:
Claire 2025-03-05 17:55:53 +01:00 committed by GitHub
parent 00dbefdbbf
commit b57687083f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 15 additions and 17 deletions

View file

@ -1,4 +1,4 @@
import { useLinks } from 'mastodon/../hooks/useLinks';
import { useLinks } from 'mastodon/hooks/useLinks';
export const AccountBio: React.FC<{
note: string;

View file

@ -1,8 +1,8 @@
import classNames from 'classnames';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import { useLinks } from 'mastodon/../hooks/useLinks';
import { Icon } from 'mastodon/components/icon';
import { useLinks } from 'mastodon/hooks/useLinks';
import type { Account } from 'mastodon/models/account';
export const AccountFields: React.FC<{

View file

@ -8,7 +8,7 @@ import type {
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
import { useSelectableClick } from '@/hooks/useSelectableClick';
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
const offset = [0, 4] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;

View file

@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
import classNames from 'classnames';
import { useHovering } from 'mastodon/../hooks/useHovering';
import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';

View file

@ -1,8 +1,7 @@
import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
import { useHovering } from '../../hooks/useHovering';
import { autoPlayGif } from '../initial_state';
interface Props {
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there

View file

@ -5,8 +5,8 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import { useTimeout } from 'mastodon/../hooks/useTimeout';
import { Icon } from 'mastodon/components/icon';
import { useTimeout } from 'mastodon/hooks/useTimeout';
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
const inputRef = useRef<HTMLTextAreaElement>(null);

View file

@ -1,4 +1,4 @@
import { useHovering } from '@/hooks/useHovering';
import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state';
export const GIF: React.FC<{

View file

@ -8,8 +8,8 @@ import type {
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
import { useTimeout } from 'mastodon/../hooks/useTimeout';
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
import { useTimeout } from 'mastodon/hooks/useTimeout';
const offset = [-12, 4] as OffsetValue;
const enterDelay = 750;

View file

@ -6,7 +6,6 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom';
import { useLinks } from '@/hooks/useLinks';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@ -46,6 +45,7 @@ import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
import AccountNoteContainer from 'mastodon/features/account/containers/account_note_container';
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
import { useLinks } from 'mastodon/hooks/useLinks';
import { useIdentity } from 'mastodon/identity_context';
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';

View file

@ -6,9 +6,9 @@ import classNames from 'classnames';
import Overlay from 'react-overlays/Overlay';
import { useSelectableClick } from '@/hooks/useSelectableClick';
import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
import { Icon } from 'mastodon/components/icon';
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
const messages = defineMessages({
help: { id: 'info_button.label', defaultMessage: 'Help' },

View file

@ -22,10 +22,9 @@ import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RadioButton } from 'mastodon/components/radio_button';
import ScrollContainer from 'mastodon/containers/scroll_container';
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { useSearchParam } from '../../../hooks/useSearchParam';
import { AccountCard } from './components/account_card';
const messages = defineMessages({

View file

@ -4,7 +4,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { useSearchParam } from '@/hooks/useSearchParam';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
@ -20,6 +19,7 @@ import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import Status from 'mastodon/containers/status_container';
import { Search } from 'mastodon/features/compose/components/search';
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
import type { Hashtag as HashtagType } from 'mastodon/models/tags';
import { useAppDispatch, useAppSelector } from 'mastodon/store';

View file

@ -6,11 +6,11 @@ import { useEffect, useCallback } from 'react';
import { Provider } from 'react-redux';
import { useRenderSignal } from 'mastodon/../hooks/useRenderSignal';
import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
import { hydrateStore } from 'mastodon/actions/store';
import { Router } from 'mastodon/components/router';
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
import { useRenderSignal } from 'mastodon/hooks/useRenderSignal';
import initialState from 'mastodon/initial_state';
import { IntlProvider } from 'mastodon/locales';
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';

View file

@ -0,0 +1,17 @@
import { useCallback, useState } from 'react';
export const useHovering = (animate?: boolean) => {
const [hovering, setHovering] = useState<boolean>(animate ?? false);
const handleMouseEnter = useCallback(() => {
if (animate) return;
setHovering(true);
}, [animate]);
const handleMouseLeave = useCallback(() => {
if (animate) return;
setHovering(false);
}, [animate]);
return { hovering, handleMouseEnter, handleMouseLeave };
};

View file

@ -0,0 +1,73 @@
import { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
import { openURL } from 'mastodon/actions/search';
import { useAppDispatch } from 'mastodon/store';
const isMentionClick = (element: HTMLAnchorElement) =>
element.classList.contains('mention');
const isHashtagClick = (element: HTMLAnchorElement) =>
element.textContent?.[0] === '#' ||
element.previousSibling?.textContent?.endsWith('#');
export const useLinks = () => {
const history = useHistory();
const dispatch = useAppDispatch();
const handleHashtagClick = useCallback(
(element: HTMLAnchorElement) => {
const { textContent } = element;
if (!textContent) return;
history.push(`/tags/${textContent.replace(/^#/, '')}`);
},
[history],
);
const handleMentionClick = useCallback(
async (element: HTMLAnchorElement) => {
const result = await dispatch(openURL({ url: element.href }));
if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(
`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`,
);
} else {
window.location.href = element.href;
}
} else if (isRejected(result)) {
window.location.href = element.href;
}
},
[dispatch, history],
);
const handleClick = useCallback(
(e: React.MouseEvent) => {
const target = (e.target as HTMLElement).closest('a');
if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) {
return;
}
if (isMentionClick(target)) {
e.preventDefault();
void handleMentionClick(target);
} else if (isHashtagClick(target)) {
e.preventDefault();
handleHashtagClick(target);
}
},
[handleMentionClick, handleHashtagClick],
);
return handleClick;
};

View file

@ -0,0 +1,32 @@
// This hook allows a component to signal that it's done rendering in a way that
// can be used by e.g. our embed code to determine correct iframe height
let renderSignalReceived = false;
type Callback = () => void;
let onInitialRender: Callback;
export const afterInitialRender = (callback: Callback) => {
if (renderSignalReceived) {
callback();
} else {
onInitialRender = callback;
}
};
export const useRenderSignal = () => {
return () => {
if (renderSignalReceived) {
return;
}
renderSignalReceived = true;
if (typeof onInitialRender !== 'undefined') {
window.requestAnimationFrame(() => {
onInitialRender();
});
}
};
};

View file

@ -0,0 +1,31 @@
import { useMemo, useCallback } from 'react';
import { useLocation, useHistory } from 'react-router';
export function useSearchParams() {
const { search } = useLocation();
return useMemo(() => new URLSearchParams(search), [search]);
}
export function useSearchParam(name: string, defaultValue?: string) {
const searchParams = useSearchParams();
const history = useHistory();
const value = searchParams.get(name) ?? defaultValue;
const setValue = useCallback(
(value: string | null) => {
if (value === null) {
searchParams.delete(name);
} else {
searchParams.set(name, value);
}
history.push({ search: searchParams.toString() });
},
[history, name, searchParams],
);
return [value, setValue] as const;
}

View file

@ -0,0 +1,55 @@
import { useRef, useCallback } from 'react';
type Position = [number, number];
export const useSelectableClick = (
onClick: React.MouseEventHandler,
maxDelta = 5,
) => {
const clickPositionRef = useRef<Position | null>(null);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
clickPositionRef.current = [e.clientX, e.clientY];
}, []);
const handleMouseUp = useCallback(
(e: React.MouseEvent) => {
if (!clickPositionRef.current) {
return;
}
const [startX, startY] = clickPositionRef.current;
const [deltaX, deltaY] = [
Math.abs(e.clientX - startX),
Math.abs(e.clientY - startY),
];
let element: EventTarget | null = e.target;
while (element && element instanceof HTMLElement) {
if (
element.localName === 'button' ||
element.localName === 'a' ||
element.localName === 'label'
) {
return;
}
element = element.parentNode;
}
if (
deltaX + deltaY < maxDelta &&
(e.button === 0 || e.button === 1) &&
e.detail >= 1
) {
onClick(e);
}
clickPositionRef.current = null;
},
[maxDelta, onClick],
);
return [handleMouseDown, handleMouseUp] as const;
};

View file

@ -0,0 +1,44 @@
import { useRef, useCallback, useEffect } from 'react';
export const useTimeout = () => {
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const callbackRef = useRef<() => void>();
const set = useCallback((callback: () => void, delay: number) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
callbackRef.current = callback;
timeoutRef.current = setTimeout(callback, delay);
}, []);
const delay = useCallback((delay: number) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (!callbackRef.current) {
return;
}
timeoutRef.current = setTimeout(callbackRef.current, delay);
}, []);
const cancel = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = undefined;
callbackRef.current = undefined;
}
}, []);
useEffect(
() => () => {
cancel();
},
[cancel],
);
return [set, cancel, delay] as const;
};