Merge remote-tracking branch 'parent/main' into upstream-20240310
This commit is contained in:
commit
5979c0ea1d
345 changed files with 4304 additions and 2540 deletions
17
app/javascript/mastodon/hooks/useHovering.ts
Normal file
17
app/javascript/mastodon/hooks/useHovering.ts
Normal 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 };
|
||||
};
|
92
app/javascript/mastodon/hooks/useLinks.ts
Normal file
92
app/javascript/mastodon/hooks/useLinks.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
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('#');
|
||||
|
||||
const isFeaturedHashtagClick = (element: HTMLAnchorElement) =>
|
||||
isHashtagClick(element) && element.href.includes('/tagged/');
|
||||
|
||||
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 handleFeaturedHashtagClick = useCallback(
|
||||
(element: HTMLAnchorElement) => {
|
||||
const { textContent, href } = element;
|
||||
|
||||
if (!textContent) return;
|
||||
|
||||
const url = new URL(href);
|
||||
|
||||
history.push(url.pathname);
|
||||
},
|
||||
[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 (isFeaturedHashtagClick(target)) {
|
||||
e.preventDefault();
|
||||
handleFeaturedHashtagClick(target);
|
||||
} else if (isHashtagClick(target)) {
|
||||
e.preventDefault();
|
||||
handleHashtagClick(target);
|
||||
}
|
||||
},
|
||||
[handleMentionClick, handleFeaturedHashtagClick, handleHashtagClick],
|
||||
);
|
||||
|
||||
return handleClick;
|
||||
};
|
32
app/javascript/mastodon/hooks/useRenderSignal.ts
Normal file
32
app/javascript/mastodon/hooks/useRenderSignal.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
31
app/javascript/mastodon/hooks/useSearchParam.ts
Normal file
31
app/javascript/mastodon/hooks/useSearchParam.ts
Normal 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;
|
||||
}
|
55
app/javascript/mastodon/hooks/useSelectableClick.ts
Normal file
55
app/javascript/mastodon/hooks/useSelectableClick.ts
Normal 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;
|
||||
};
|
44
app/javascript/mastodon/hooks/useTimeout.ts
Normal file
44
app/javascript/mastodon/hooks/useTimeout.ts
Normal 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;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue