Merge commit 'e2b7096bd3f93afcf4d6b3e7ffb9ccdb417a9def' into glitch-soc/merge-4.6
This commit is contained in:
commit
6aca68e185
10
CHANGELOG.md
10
CHANGELOG.md
@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Added
|
||||
|
||||
- **Add collections** (#37992, #37005, #37049, #37020, #37053, #37110, #37117, #37122, #37154, #37157, #37176, #37192, #37222, #37225, #37254, #37277, #37298, #37322, #37434, #37468, #37514, #37512, #37549, #37556, #37560, #37580, #37591, #37552, #37618, #37643, #37658, #37731, #37678, #37741, #37762, #37790, #37805, #37823, #37837, #37842, #37850, #37848, #37812, #37950, #37898, #37916, #37920, #37927, #37928, #37961, #37967, #37974, #37989, #37986, #38004, #38026, #38027, #38030, #38038, #38065, #38081, #38082, #38096, #38106, #38113, #38124, #38133, #38144, #38153, #38166, #38167, #38169, #38170, #38177, #38193, #38213, #38251, #38255, #38256, #38282, #38298, #38292, #38307, #38306, #38316, #38115, #38329, #38334, #38337, #38351, #38368, #38370, #38356, #38383, #38386, #38385, #38394, #38393, #38399, #38402, #38409, #38414, #38413, #38424, #38425, #38450, #38508, #38528, #38534, #38536, #38540, #38543, #38491, #38586, #38611, #38588, #38612, #38628, #38626, #38630, #38633, #38629, #38638, #38645, #38644, #38636, #38660, #38657, #38688, #38690, #38672, #38698, #38697, #38708, #38712, #38713, #38709, #38719, #38728, #38730, #38732, #38739, #38749, #38751, #38750, #38767, #38769, #38783, #38785, #38959, #38786, #38794, #38776, #38817, #38792, #38822, #38827, #38831, #38830, #38844, #38843, #38852, #38850, #38847, #38865, #38897, #38900, #38919, #38933, #38934, #38935, #38942, #38941, #38954, #38961, #38957, #38962, #38991, #39009, #39062, #39029, #39069, #39020, #39073, #39082, #39096, #39080, #39182, #39143, #39127, #37929, #38029, #39194, #39198, #39210, #39211, #39202, #39214, #39215, #39220, #39234, #39260, #39251, #39361, #39357, #39349, #39287, #39376, #39289, #39342, #38711, #39379, #39282, #39286, #39296, #39047, #39346, #39373, #39372 by @ChaosExAnima, @ClearlyClaire, @Gargron, @arte7, @diondiondion, @mjankowski, @oneiros, and @shleeable)
|
||||
- **Add collections** (#37992, #37005, #37049, #37020, #37053, #37110, #37117, #37122, #37154, #37157, #37176, #37192, #37222, #37225, #37254, #37277, #37298, #37322, #37434, #37468, #37514, #37512, #37549, #37556, #37560, #37580, #37591, #37552, #37618, #37643, #37658, #37731, #37678, #37741, #37762, #37790, #37805, #37823, #37837, #37842, #37850, #37848, #37812, #37950, #37898, #37916, #37920, #37927, #37928, #37961, #37967, #37974, #37989, #37986, #38004, #38026, #38027, #38030, #38038, #38065, #38081, #38082, #38096, #38106, #38113, #38124, #38133, #38144, #38153, #38166, #38167, #38169, #38170, #38177, #38193, #38213, #38251, #38255, #38256, #38282, #38298, #38292, #38307, #38306, #38316, #38115, #38329, #38334, #38337, #38351, #38368, #38370, #38356, #38383, #38386, #38385, #38394, #38393, #38399, #38402, #38409, #38414, #38413, #38424, #38425, #38450, #38508, #38528, #38534, #38536, #38540, #38543, #38491, #38586, #38611, #38588, #38612, #38628, #38626, #38630, #38633, #38629, #38638, #38645, #38644, #38636, #38660, #38657, #38688, #38690, #38672, #38698, #38697, #38708, #38712, #38713, #38709, #38719, #38728, #38730, #38732, #38739, #38749, #38751, #38750, #38767, #38769, #38783, #38785, #38959, #38786, #38794, #38776, #38817, #38792, #38822, #38827, #38831, #38830, #38844, #38843, #38852, #38850, #38847, #38865, #38897, #38900, #38919, #38933, #38934, #38935, #38942, #38941, #38954, #38961, #38957, #38962, #38991, #39009, #39062, #39029, #39069, #39020, #39073, #39082, #39096, #39080, #39182, #39143, #39127, #37929, #38029, #39194, #39198, #39210, #39211, #39202, #39214, #39215, #39220, #39234, #39260, #39251, #39361, #39357, #39349, #39287, #39376, #39289, #39342, #38711, #39379, #39282, #39286, #39296, #39047, #39346, #39373, #39372, and #39429 by @ChaosExAnima, @ClearlyClaire, @Gargron, @arte7, @diondiondion, @mjankowski, @oneiros, and @shleeable)
|
||||
- Create collections with up to 25 accounts each, then share them with others. You can read more about this feature [on our blog](https://blog.joinmastodon.org/2026/04/designing-collections/). This is based on FEP-7aa9 (Featured Collections) to be interoperable with the wider Fediverse. All the new API methods [are documented here](https://docs.joinmastodon.org/client/collections/).
|
||||
- **Add email subscriptions** (#38163, #38507, #38502, #38487, #38527, #38582, #38741, #38907, #39162, #39271 by @ClearlyClaire and @Gargron)
|
||||
- Admins can allow specific roles to enable email subscriptions on their profile, allowing anonymous visitors to subscribe to their posts via email.
|
||||
@ -27,7 +27,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Add fallback attributes to notifications for new and infrequent notifications in REST API (#38832 and #38860 by @ClearlyClaire)
|
||||
- This adds a [`supported_types`](https://docs.joinmastodon.org/methods/notifications/#query-parameters-1) parameter to `GET /api/v1/notifications`, `GET /api/v1/notifications/:id`, `GET /api/v2/notifications`, and `GET /api/v2/notifications/:group_key` along with a new `fallback` attribute for notifications and notification groups.
|
||||
- Add support for posts in vertical languages in web UI (#37204, #38205, and #38797 by @shimon1024)
|
||||
- Add `PageUp` and `PageDown` hotkeys for list navigation (#39252 by @diondiondion)
|
||||
- Add `Alt` + `PageUp` and `Alt` + `PageDown` hotkeys for list navigation (#39252 and #39427 by @diondiondion)
|
||||
- Add `g`+`e` keyboard shortcut to access the trending page in web UI (#38014 by @antoinecellerier)
|
||||
- Add `Cmd`/`Ctrl`+`Enter` for form submissions in more text areas in web UI (#37821 by @diondiondion)
|
||||
- Add support for quoting by dragging a link into the compose form in web UI (#36859 and #36896 by @ClearlyClaire and @tribela)
|
||||
@ -57,7 +57,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Change limitation to allow posts with both media and a poll to be created (#39203, #39368, #39388 by @ClearlyClaire and @Gargron)
|
||||
- Change alt text limit for media attachments to 10,000 characters (#39306 by @ClearlyClaire)
|
||||
- Change pending user notification email to link directly to the pending account (#39206 by @vmstan)
|
||||
- Changed emoji processing in web UI to make it less resource intensive and more robust (#39077, #39008, #39088, #38892, #38885, #38965, #38854, #38825, #38784, #38541, #37442, #37300, #37306, #37271, #37255, #37284, #37272, #37178, #37084, #37080, #37418, #39167, #39126, #39353, #39378, #39382 by @ChaosExAnima, @ClearlyClaire, @diondiondion, and @gomasy)
|
||||
- Changed emoji processing in web UI to make it less resource intensive and more robust (#39077, #39008, #39088, #38892, #38885, #38965, #38854, #38825, #38784, #38541, #37442, #37300, #37306, #37271, #37255, #37284, #37272, #37178, #37084, #37080, #37418, #39167, #39126, #39353, #39378, #39382, #39402, and #39421 by @ChaosExAnima, @ClearlyClaire, @diondiondion, @gomasy, and @Hanage999)
|
||||
- Change composer textarea to have a limited height to prevent column scrolling (#39268 by @diondiondion)
|
||||
- Change mentions of “Mastodon gGmbH” to “Mastodon GmbH” (#39261 by @renchap)
|
||||
- Change the limited profile message to be less misleading (#39231 by @mortie)
|
||||
@ -102,7 +102,8 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix accessibility issues in web UI (#37250, #38006, #38033, #38188, #38230, #38252, #38257, #38285, #38293, #38362, #38387, #38459, #38796, #38801, #39098, #39111, #39120, #39129, #39133, #39134, #39144, #39145, #39149, #39164, #39165, #39169, #39181, #39335, #39305, #39331, #39356, #39350, #39358, #39360, #39325, #39270 by @ChaosExAnima and @diondiondion)
|
||||
- Fix accessibility issues in web UI (#37250, #38006, #38033, #38188, #38230, #38252, #38257, #38285, #38293, #38362, #38387, #38459, #38796, #38801, #39098, #39111, #39120, #39129, #39133, #39134, #39144, #39145, #39149, #39164, #39165, #39169, #39181, #39335, #39305, #39331, #39356, #39350, #39358, #39360, #39325, #39270, #39439, #39400, and #39408 by @ChaosExAnima and @diondiondion)
|
||||
- Fix error with audio player in Safari Lockdown Mode (#39397 by @Federicorao)
|
||||
- Fix tiny checkboxes and radio buttons in Safari (#39332 by @diondiondion)
|
||||
- Fix handling of offset in timezone list in settings (#39334 by @mjankowski)
|
||||
- Fix being unable to unmark media as sensitive when "always mark media as sensitive" is enabled in web UI (#39339 by @matrix07012)
|
||||
@ -110,6 +111,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Fix some inputs incorrectly having resize handles in Firefox (#39274 by @diondiondion)
|
||||
- Fix processing some link previews where text is language-tagged (#39190 by @zunda)
|
||||
- Fix error when “New trends” email is sent at the same time trends are recomputed (#39122 by @arte7)
|
||||
- Fix hovercard not showing in compose column (#39430 by @diondiondion)
|
||||
- Fix hover card opening even when not preceded by mouse movement in web UI (#39166, #39381 by @diondiondion)
|
||||
- Fix [ominous](https://mastodon.social/@mcc/116404362104299129) "Moments remaining" timestamp in web UI (#38488 and #38689 by @ChaosExAnima and @MitarashiDango)
|
||||
- Fix filters not being applied to search results in web UI (#36346 by @ClearlyClaire)
|
||||
|
||||
@ -456,7 +456,7 @@ GEM
|
||||
mutex_m (0.3.0)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-imap (0.6.4)
|
||||
net-imap (0.6.4.1)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.20.0)
|
||||
|
||||
@ -5,6 +5,7 @@ import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { importFetchedStatuses } from '@/mastodon/actions/importer';
|
||||
import { hydrateStore } from '@/mastodon/actions/store';
|
||||
import type { ApiAnnualReportResponse } from '@/mastodon/api/annual_report';
|
||||
import { FocusTargetProvider } from '@/mastodon/components/navigation_focus_target';
|
||||
import { Router } from '@/mastodon/components/router';
|
||||
import { WrapstodonSharedPage } from '@/mastodon/features/annual_report/shared_page';
|
||||
import { IntlProvider, loadLocale } from '@/mastodon/locales';
|
||||
@ -52,7 +53,9 @@ function loaded() {
|
||||
<IntlProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<Router>
|
||||
<WrapstodonSharedPage />
|
||||
<FocusTargetProvider>
|
||||
<WrapstodonSharedPage />
|
||||
</FocusTargetProvider>
|
||||
</Router>
|
||||
</ReduxProvider>
|
||||
</IntlProvider>,
|
||||
|
||||
@ -22,10 +22,6 @@ export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) {
|
||||
await clearCache('custom');
|
||||
await loadCustomEmoji();
|
||||
|
||||
const { reloadCustomEmojis } =
|
||||
await import('@/mastodon/features/emoji/picker');
|
||||
await reloadCustomEmojis();
|
||||
|
||||
log('Custom emojis updated, reloaded cache and picker data.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,6 +278,7 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
|
||||
font-size: 15px;
|
||||
color: var(--color-text-primary);
|
||||
unicode-bidi: plaintext;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { normalizeKey, isKeyboardEvent } from './utils';
|
||||
import { normalizeKey, isKeyboardEvent, matchesKeyCode } from './utils';
|
||||
|
||||
/**
|
||||
* In case of multiple hotkeys matching the pressed key(s),
|
||||
@ -55,6 +55,30 @@ function any(...keys: string[]): KeyMatcher {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches any matcher function out of those provided
|
||||
*/
|
||||
function anyMatcher(...matchers: KeyMatcher[]): KeyMatcher {
|
||||
return (event) => {
|
||||
let match: ReturnType<KeyMatcher> | undefined;
|
||||
|
||||
for (const matcher of matchers) {
|
||||
const matcherResult = matcher(event);
|
||||
if (matcherResult.isMatch) {
|
||||
match = matcherResult;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
match ?? {
|
||||
isMatch: false,
|
||||
priority: hotkeyPriority.singleKey,
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a single key combined with the option/alt modifier
|
||||
*/
|
||||
@ -62,7 +86,7 @@ function optionPlus(key: string): KeyMatcher {
|
||||
return (event) => ({
|
||||
// Matching against event.code here as alt combos are often
|
||||
// mapped to other characters
|
||||
isMatch: event.altKey && event.code === `Key${key.toUpperCase()}`,
|
||||
isMatch: event.altKey && matchesKeyCode(key, event.code),
|
||||
priority: hotkeyPriority.combo,
|
||||
});
|
||||
}
|
||||
@ -109,8 +133,8 @@ const hotkeyMatcherMap = {
|
||||
mention: just('m'),
|
||||
open: any('enter', 'o'),
|
||||
openProfile: just('p'),
|
||||
moveDown: any('j', 'pagedown'),
|
||||
moveUp: any('k', 'pageup'),
|
||||
moveDown: anyMatcher(just('j'), optionPlus('pagedown')),
|
||||
moveUp: anyMatcher(just('k'), optionPlus('pageup')),
|
||||
moveToTop: just('0'),
|
||||
toggleHidden: just('x'),
|
||||
toggleSensitive: just('h'),
|
||||
@ -188,7 +212,7 @@ export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
|
||||
|
||||
if (shouldHandleEvent) {
|
||||
const matchCandidates: {
|
||||
// A candidate will be have an undefined handler if it's matched,
|
||||
// A candidate can have an undefined handler if it's matched,
|
||||
// but handled in a parent component rather than this one.
|
||||
handler: HandlerFunction | undefined;
|
||||
priority: number;
|
||||
|
||||
@ -2,6 +2,9 @@ export function isKeyboardEvent(event: Event): event is KeyboardEvent {
|
||||
return 'key' in event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalises key values to consistent lowercase strings
|
||||
*/
|
||||
export function normalizeKey(key: string): string {
|
||||
const lowerKey = key.toLowerCase();
|
||||
|
||||
@ -27,3 +30,16 @@ export function normalizeKey(key: string): string {
|
||||
return lowerKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare whether a key matches an `event.code` value,
|
||||
* with support for single-letter keys (which are otherwise
|
||||
* represented with a `Key` prefix, e.g. `m` is `KeyM`).
|
||||
*/
|
||||
export function matchesKeyCode(key: string, code: string) {
|
||||
if (key.length === 1) {
|
||||
return code === `Key${key.toUpperCase()}`;
|
||||
} else {
|
||||
return code.toLowerCase() === key.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,13 +55,11 @@ export const FocusTargetProvider: React.FC<{
|
||||
| null
|
||||
>(null);
|
||||
|
||||
const {
|
||||
pathname,
|
||||
search,
|
||||
state = {},
|
||||
} = useLocation<{ focusTarget?: FocusTarget } | undefined>();
|
||||
const { pathname, search, state } = useLocation<{
|
||||
focusTarget?: FocusTarget;
|
||||
} | null>();
|
||||
|
||||
const { focusTarget } = state;
|
||||
const { focusTarget } = state ?? {};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// We never want to set focus on page load, so we keep
|
||||
@ -102,15 +100,9 @@ export const FocusTargetProvider: React.FC<{
|
||||
export function useFocusOnNavigation(targetName?: string) {
|
||||
const focusTargetRef = useContext(FocusTargetContext);
|
||||
|
||||
if (focusTargetRef === null) {
|
||||
throw Error(
|
||||
'useFocusTargetContext must be used inside of a FocusTargetProvider',
|
||||
);
|
||||
}
|
||||
|
||||
return useCallback(
|
||||
(element: HTMLElement | null) => {
|
||||
const focusTarget = focusTargetRef.current;
|
||||
const focusTarget = focusTargetRef?.current;
|
||||
|
||||
// Bail out if focusTarget was set to `false`
|
||||
if (!element || !focusTarget) {
|
||||
|
||||
@ -140,7 +140,7 @@ export const EditFieldModal = forwardRef<
|
||||
|
||||
const customEmojis = useCustomEmojis();
|
||||
const customEmojiCodes = useMemo(
|
||||
() => Object.keys(customEmojis ?? {}),
|
||||
() => Object.keys(customEmojis),
|
||||
[customEmojis],
|
||||
);
|
||||
const checkField = useCallback(
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
|
||||
.bio {
|
||||
unicode-bidi: plaintext;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
p:not(:last-child) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
@ -323,10 +323,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||
EmojiPickerAsync().then(EmojiMart => {
|
||||
EmojiPicker = EmojiMart.Picker;
|
||||
Emoji = EmojiMart.Emoji;
|
||||
|
||||
void EmojiMart.loadCustomEmojiData().then(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
this.setState({ loading: false });
|
||||
}).catch(() => {
|
||||
this.setState({ loading: false, active: false });
|
||||
});
|
||||
|
||||
@ -13,8 +13,6 @@ import { usePickerEmojis } from './picker';
|
||||
|
||||
const backgroundImageFnDefault = () => `${assetHost}/emoji/sheet_16_0.png`;
|
||||
|
||||
export { fetchCustomEmojiData as loadCustomEmojiData } from './picker';
|
||||
|
||||
export const Picker: FC<PickerProps> = ({
|
||||
set = 'twitter',
|
||||
sheetSize = 32,
|
||||
@ -24,17 +22,13 @@ export const Picker: FC<PickerProps> = ({
|
||||
...props
|
||||
}) => {
|
||||
const { mode } = useEmojiAppState();
|
||||
const { customCategories, customEmojis } = usePickerEmojis();
|
||||
|
||||
if (!customEmojis) {
|
||||
return null;
|
||||
}
|
||||
const { categories, emojis } = usePickerEmojis();
|
||||
|
||||
return (
|
||||
<PickerRaw
|
||||
data={EmojiData}
|
||||
custom={customEmojis}
|
||||
include={customCategories}
|
||||
custom={emojis}
|
||||
include={categories}
|
||||
set={set}
|
||||
sheetSize={sheetSize}
|
||||
sheetColumns={sheetColumns}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { initialState } from '@/mastodon/initial_state';
|
||||
|
||||
import { toSupportedLocale } from './locale';
|
||||
import { reloadCustomEmojis } from './picker';
|
||||
import type { EmojiWorkerMessage } from './types';
|
||||
import { emojiLogger } from './utils';
|
||||
|
||||
@ -39,27 +38,37 @@ export async function initializeEmoji() {
|
||||
void fallbackLoad();
|
||||
}, WORKER_TIMEOUT);
|
||||
|
||||
tempWorker.addEventListener('message', (event: MessageEvent<string>) => {
|
||||
const { data: message } = event;
|
||||
tempWorker.addEventListener(
|
||||
'message',
|
||||
(event: MessageEvent<EmojiWorkerMessage>) => {
|
||||
const { data: message } = event;
|
||||
|
||||
worker ??= tempWorker;
|
||||
clearTimeout(timeoutId);
|
||||
worker ??= tempWorker;
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (message !== 'ready') {
|
||||
workerLog(message);
|
||||
return;
|
||||
}
|
||||
const { type } = message;
|
||||
if (type === 'log') {
|
||||
workerLog(message.message);
|
||||
} else if (type === 'done' && message.storeName === 'custom') {
|
||||
void loadEmojisToStore();
|
||||
}
|
||||
|
||||
const debugValue = localStorage.getItem('debug');
|
||||
if (debugValue) {
|
||||
messageWorker({ type: 'debug', debugValue });
|
||||
}
|
||||
if (type !== 'ready') {
|
||||
return; // Exit for other messages.
|
||||
}
|
||||
|
||||
workerLog('loading data');
|
||||
messageWorker(userLocale);
|
||||
messageWorker('custom');
|
||||
messageWorker('shortcodes');
|
||||
});
|
||||
const debugValue = localStorage.getItem('debug');
|
||||
if (debugValue) {
|
||||
messageWorker({ type: 'debug', debugValue });
|
||||
}
|
||||
|
||||
workerLog('loading data');
|
||||
messageWorker(userLocale);
|
||||
messageWorker('custom');
|
||||
messageWorker('shortcodes');
|
||||
void loadEmojisToStore();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function fallbackLoad() {
|
||||
@ -71,8 +80,8 @@ async function fallbackLoad() {
|
||||
const customEmojis = await importCustomEmojiData();
|
||||
if (customEmojis && customEmojis.length > 0) {
|
||||
log('loaded %d custom emojis', customEmojis.length);
|
||||
await reloadCustomEmojis();
|
||||
}
|
||||
|
||||
const shortcodes = await importLegacyShortcodes();
|
||||
if (shortcodes?.length) {
|
||||
log('loaded %d legacy shortcodes', shortcodes.length);
|
||||
@ -82,6 +91,7 @@ async function fallbackLoad() {
|
||||
if (emojis) {
|
||||
log('loaded %d emojis to locale %s', emojis.length, userLocale);
|
||||
}
|
||||
await loadEmojisToStore();
|
||||
}
|
||||
|
||||
export async function loadCustomEmoji() {
|
||||
@ -92,9 +102,9 @@ export async function loadCustomEmoji() {
|
||||
const emojis = await importCustomEmojiData();
|
||||
if (emojis && emojis.length > 0) {
|
||||
log('loaded %d custom emojis', emojis.length);
|
||||
await reloadCustomEmojis();
|
||||
}
|
||||
}
|
||||
await loadEmojisToStore();
|
||||
}
|
||||
|
||||
function messageWorker(data: EmojiWorkerMessage | string) {
|
||||
@ -110,3 +120,14 @@ function messageWorker(data: EmojiWorkerMessage | string) {
|
||||
worker.postMessage(data);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEmojisToStore() {
|
||||
const { store } = await import('@/mastodon/store');
|
||||
const { loadCustomEmojis, loadLocale } =
|
||||
await import('@/mastodon/reducers/slices/emojis');
|
||||
|
||||
loadLocale(userLocale);
|
||||
await store.dispatch(loadCustomEmojis());
|
||||
|
||||
log('loaded emoji data into store');
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@ export function useEmojiAppState(): EmojiAppState {
|
||||
|
||||
return {
|
||||
currentLocale: locale,
|
||||
locales: [locale],
|
||||
mode,
|
||||
darkTheme: isDarkMode(),
|
||||
assetHost,
|
||||
@ -64,7 +63,6 @@ export function getEmojiAppState(): EmojiAppState {
|
||||
|
||||
return {
|
||||
currentLocale,
|
||||
locales: [currentLocale],
|
||||
mode: determineEmojiMode(emojiStyle),
|
||||
darkTheme: isDarkMode(),
|
||||
assetHost,
|
||||
|
||||
@ -1,83 +1,18 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { CategoryName, CustomEmoji } from 'emoji-mart';
|
||||
|
||||
import { autoPlayGif } from '@/mastodon/initial_state';
|
||||
import {
|
||||
createAppSelector,
|
||||
useAppSelector,
|
||||
} from '@/mastodon/store/typed_functions';
|
||||
import { createLimitedCache } from '@/mastodon/utils/cache';
|
||||
|
||||
import { emojiLogger } from './utils';
|
||||
|
||||
const log = emojiLogger('picker');
|
||||
|
||||
let customEmojis: CustomEmoji[] | null = null;
|
||||
let customCategories = [
|
||||
'recent',
|
||||
'people',
|
||||
'nature',
|
||||
'foods',
|
||||
'activity',
|
||||
'places',
|
||||
'objects',
|
||||
'symbols',
|
||||
'flags',
|
||||
] as CategoryName[];
|
||||
|
||||
const searchCache = createLimitedCache<LegacyEmoji[]>({ maxSize: 10, log });
|
||||
|
||||
export async function fetchCustomEmojiData() {
|
||||
if (customEmojis !== null) {
|
||||
return customEmojis;
|
||||
}
|
||||
|
||||
const { loadAllCustomEmoji } = await import('./database');
|
||||
const emojisRaw = await loadAllCustomEmoji();
|
||||
|
||||
// If it returns null then custom emojis aren't even loaded yet.
|
||||
if (emojisRaw === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If it's empty, then they are loaded but there aren't any.
|
||||
if (emojisRaw.length === 0) {
|
||||
customEmojis = [];
|
||||
return customEmojis;
|
||||
}
|
||||
|
||||
const categories = new Set(['custom']);
|
||||
const emojis = [];
|
||||
for (const emoji of emojisRaw) {
|
||||
const name = emoji.shortcode.replaceAll(':', '');
|
||||
emojis.push({
|
||||
name,
|
||||
id: name,
|
||||
custom: true,
|
||||
short_names: [name],
|
||||
imageUrl: autoPlayGif ? emoji.url : emoji.static_url,
|
||||
customCategory: emoji.category,
|
||||
});
|
||||
|
||||
if (emoji.category) {
|
||||
categories.add(`custom-${emoji.category}`);
|
||||
}
|
||||
}
|
||||
|
||||
customEmojis = emojis.toSorted((a, b) => {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
});
|
||||
customCategories = customCategories.toSpliced(
|
||||
1,
|
||||
0,
|
||||
...(Array.from(categories).toSorted() as CategoryName[]),
|
||||
);
|
||||
log(
|
||||
'loaded %d custom emojis in %d categories',
|
||||
customEmojis.length,
|
||||
categories.size,
|
||||
);
|
||||
|
||||
return customEmojis;
|
||||
}
|
||||
|
||||
type LegacyEmoji =
|
||||
| { id: string; custom?: false; native: string }
|
||||
| {
|
||||
@ -85,16 +20,6 @@ type LegacyEmoji =
|
||||
custom: true;
|
||||
};
|
||||
|
||||
export async function reloadCustomEmojis() {
|
||||
customEmojis = null;
|
||||
|
||||
const { loadEmojisIntoCache } =
|
||||
await import('@/mastodon/hooks/useCustomEmojis');
|
||||
|
||||
await Promise.all([fetchCustomEmojiData(), loadEmojisIntoCache()]);
|
||||
searchCache.clear();
|
||||
}
|
||||
|
||||
// Replicates the old legacy search function.
|
||||
export async function emojiMartSearch(
|
||||
token: string,
|
||||
@ -127,19 +52,66 @@ export async function emojiMartSearch(
|
||||
return legacyResults;
|
||||
}
|
||||
|
||||
export function usePickerEmojis() {
|
||||
const [, setLoaded] = useState(customEmojis !== null);
|
||||
const defaultCategories = [
|
||||
'people',
|
||||
'nature',
|
||||
'foods',
|
||||
'activity',
|
||||
'places',
|
||||
'objects',
|
||||
'symbols',
|
||||
'flags',
|
||||
] as CategoryName[];
|
||||
|
||||
useEffect(() => {
|
||||
if (customEmojis === null) {
|
||||
void fetchCustomEmojiData().then(() => {
|
||||
setLoaded(true);
|
||||
});
|
||||
const selectPickerData = createAppSelector(
|
||||
[(state) => state.emojis.custom, (state) => state.emojis.customCategories],
|
||||
(emojis, categories) => {
|
||||
// Create a map of shortcode to category name.
|
||||
const categoryMap = new Map<string, string>();
|
||||
for (const category in categories) {
|
||||
const catEmojis = categories[category];
|
||||
if (!catEmojis?.length) {
|
||||
continue;
|
||||
}
|
||||
for (const shortcode of catEmojis) {
|
||||
categoryMap.set(shortcode, category);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
customEmojis,
|
||||
customCategories,
|
||||
};
|
||||
const customEmojis: CustomEmoji[] = [];
|
||||
for (const shortcode in emojis) {
|
||||
const emoji = emojis[shortcode];
|
||||
if (!emoji) {
|
||||
continue;
|
||||
}
|
||||
|
||||
customEmojis.push({
|
||||
name: shortcode,
|
||||
id: shortcode,
|
||||
custom: true,
|
||||
short_names: [shortcode],
|
||||
imageUrl: autoPlayGif ? emoji.url : emoji.static_url,
|
||||
customCategory: categoryMap.get(shortcode),
|
||||
} as CustomEmoji);
|
||||
}
|
||||
|
||||
searchCache.clear();
|
||||
log('regenerated the picker data');
|
||||
|
||||
return {
|
||||
emojis: customEmojis,
|
||||
categories: [
|
||||
'recent',
|
||||
'custom',
|
||||
...Object.keys(categories)
|
||||
.toSorted()
|
||||
.map((category) => `custom-${category}`),
|
||||
...defaultCategories,
|
||||
] as CategoryName[],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export function usePickerEmojis() {
|
||||
return useAppSelector(selectPickerData);
|
||||
}
|
||||
|
||||
@ -109,7 +109,7 @@ export async function updateHtmlWithEmoji({
|
||||
}: {
|
||||
element: Element;
|
||||
locale: string;
|
||||
} & Omit<EmojiAppState, 'currentLocale' | 'locales'>) {
|
||||
} & Omit<EmojiAppState, 'currentLocale'>) {
|
||||
if (mode === EMOJI_MODE_NATIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -28,7 +28,6 @@ export type CacheKey =
|
||||
| LocaleWithShortcodes;
|
||||
|
||||
export interface EmojiAppState {
|
||||
locales: Locale[];
|
||||
currentLocale: Locale;
|
||||
mode: EmojiMode;
|
||||
darkTheme: boolean;
|
||||
@ -82,10 +81,20 @@ export type ExtraCustomEmojiMap = Record<
|
||||
>;
|
||||
|
||||
export type EmojiWorkerMessage =
|
||||
| { type: 'ready' }
|
||||
| {
|
||||
type: 'load';
|
||||
storeName: string;
|
||||
}
|
||||
| {
|
||||
type: 'done';
|
||||
storeName: string;
|
||||
importCount: number;
|
||||
}
|
||||
| {
|
||||
type: 'log';
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: 'debug';
|
||||
debugValue: string;
|
||||
|
||||
@ -9,13 +9,13 @@ import {
|
||||
import type { EmojiWorkerMessage } from './types';
|
||||
|
||||
addEventListener('message', handleMessage);
|
||||
self.postMessage('ready'); // After the worker is ready, notify the main thread
|
||||
self.postMessage({ type: 'ready' } satisfies EmojiWorkerMessage); // After the worker is ready, notify the main thread
|
||||
|
||||
function handleMessage(event: MessageEvent<EmojiWorkerMessage>) {
|
||||
const { data } = event;
|
||||
if (data.type === 'debug') {
|
||||
debug.enable(data.debugValue);
|
||||
} else {
|
||||
} else if (data.type === 'load') {
|
||||
void loadData(data.storeName);
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,10 @@ async function loadData(storeName: string) {
|
||||
}
|
||||
|
||||
if (importCount) {
|
||||
self.postMessage(`loaded ${importCount} emojis into ${storeName}`);
|
||||
self.postMessage({
|
||||
type: 'done',
|
||||
storeName,
|
||||
importCount,
|
||||
} satisfies EmojiWorkerMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,11 +88,11 @@ class KeyboardShortcuts extends ImmutablePureComponent {
|
||||
<td><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>k</kbd>, <FormattedMessage id='keyboard_shortcuts.keys.page_up' defaultMessage='Page Up' tagName='kbd' /></td>
|
||||
<td><kbd>k</kbd>, <FormattedMessage id='keyboard_shortcuts.keys.alt' defaultMessage='Alt' tagName='kbd' />+<FormattedMessage id='keyboard_shortcuts.keys.page_up' defaultMessage='Page Up' tagName='kbd' /></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>j</kbd>, <FormattedMessage id='keyboard_shortcuts.keys.page_down' defaultMessage='Page Down' tagName='kbd' /></td>
|
||||
<td><kbd>j</kbd>, <FormattedMessage id='keyboard_shortcuts.keys.alt' defaultMessage='Alt' tagName='kbd' />+<FormattedMessage id='keyboard_shortcuts.keys.page_down' defaultMessage='Page Down' tagName='kbd' /></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@ -8,6 +8,7 @@ import { Provider } from 'react-redux';
|
||||
|
||||
import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
||||
import { hydrateStore } from 'mastodon/actions/store';
|
||||
import { FocusTargetProvider } from 'mastodon/components/navigation_focus_target';
|
||||
import { Router } from 'mastodon/components/router';
|
||||
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
|
||||
import { useRenderSignal } from 'mastodon/hooks/useRenderSignal';
|
||||
@ -79,7 +80,9 @@ export const Status: React.FC<{ id: string }> = ({ id }) => {
|
||||
<IntlProvider>
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<Embed id={id} />
|
||||
<FocusTargetProvider>
|
||||
<Embed id={id} />
|
||||
</FocusTargetProvider>
|
||||
</Router>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
|
||||
@ -18,7 +18,7 @@ export const useAudioContext = ({ audioElementRef }: AudioContextOptions) => {
|
||||
const gainNodeRef = useRef<GainNode>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!audioElementRef.current) {
|
||||
if (!audioElementRef.current || typeof AudioContext === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -43,13 +43,17 @@ export const useAudioContext = ({ audioElementRef }: AudioContextOptions) => {
|
||||
}, [audioElementRef]);
|
||||
|
||||
const playAudio = useCallback(() => {
|
||||
void audioElementRef.current?.play();
|
||||
void audioContextRef.current?.resume();
|
||||
if (audioContextRef.current && audioElementRef.current) {
|
||||
void audioElementRef.current.play();
|
||||
void audioContextRef.current.resume();
|
||||
}
|
||||
}, [audioElementRef]);
|
||||
|
||||
const pauseAudio = useCallback(() => {
|
||||
audioElementRef.current?.pause();
|
||||
void audioContextRef.current?.suspend();
|
||||
if (audioContextRef.current && audioElementRef.current) {
|
||||
audioElementRef.current.pause();
|
||||
void audioContextRef.current.suspend();
|
||||
}
|
||||
}, [audioElementRef]);
|
||||
|
||||
return {
|
||||
|
||||
@ -1,40 +1,25 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import type { ExtraCustomEmojiMap } from '../features/emoji/types';
|
||||
import { emojiLogger } from '../features/emoji/utils';
|
||||
|
||||
let emojis: ExtraCustomEmojiMap | null = null;
|
||||
|
||||
const log = emojiLogger('useCustomEmojis');
|
||||
const selectCustomEmojis = createAppSelector(
|
||||
[(state) => state.emojis.custom],
|
||||
(custom) => {
|
||||
const emojis: ExtraCustomEmojiMap = {};
|
||||
for (const shortcode in custom) {
|
||||
const emoji = custom[shortcode];
|
||||
if (!emoji) {
|
||||
continue;
|
||||
}
|
||||
emojis[shortcode] = {
|
||||
shortcode,
|
||||
...emoji,
|
||||
};
|
||||
}
|
||||
return emojis;
|
||||
},
|
||||
);
|
||||
|
||||
export function useCustomEmojis() {
|
||||
const [, setLoaded] = useState(emojis !== null);
|
||||
useEffect(() => {
|
||||
if (!emojis) {
|
||||
void loadEmojisIntoCache().then(() => {
|
||||
setLoaded(true);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return emojis;
|
||||
}
|
||||
|
||||
export async function loadEmojisIntoCache() {
|
||||
const { loadAllCustomEmoji } = await import('../features/emoji/database');
|
||||
const emojisRaw = await loadAllCustomEmoji();
|
||||
if (emojisRaw === null) {
|
||||
log('Custom emojis not loaded yet');
|
||||
return;
|
||||
}
|
||||
|
||||
emojis = {};
|
||||
for (const emoji of emojisRaw) {
|
||||
emojis[emoji.shortcode] = {
|
||||
url: emoji.url,
|
||||
shortcode: emoji.shortcode,
|
||||
static_url: emoji.static_url,
|
||||
};
|
||||
}
|
||||
log('Loaded %d custom emojis into cache', Object.keys(emojis).length);
|
||||
return useAppSelector(selectCustomEmojis);
|
||||
}
|
||||
|
||||
71
app/javascript/mastodon/reducers/slices/emojis.ts
Normal file
71
app/javascript/mastodon/reducers/slices/emojis.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import type { Locale } from 'emojibase';
|
||||
|
||||
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji';
|
||||
import { toSupportedLocale } from '@/mastodon/features/emoji/locale';
|
||||
import { createAsyncThunk } from '@/mastodon/store/typed_functions';
|
||||
|
||||
interface EmojisState {
|
||||
custom: Record<string, Pick<ApiCustomEmojiJSON, 'url' | 'static_url'>>;
|
||||
customCategories: Record<string, string[]>; // { name: shortcodes[] }
|
||||
customLoaded: boolean;
|
||||
localesLoaded: Locale[];
|
||||
}
|
||||
|
||||
const emojisSlice = createSlice({
|
||||
name: 'emojis',
|
||||
initialState: {
|
||||
custom: {},
|
||||
customCategories: {},
|
||||
customLoaded: false,
|
||||
localesLoaded: [],
|
||||
} as EmojisState,
|
||||
reducers: {
|
||||
loadLocale(state, action: PayloadAction<string>) {
|
||||
const locale = toSupportedLocale(action.payload);
|
||||
if (!state.localesLoaded.includes(locale)) {
|
||||
state.localesLoaded.push(locale);
|
||||
}
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addAsyncThunk(loadCustomEmojis, {
|
||||
fulfilled(state, action) {
|
||||
if (!action.payload?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const emoji of action.payload) {
|
||||
const { shortcode, category, url, static_url } = emoji;
|
||||
state.custom[shortcode] = {
|
||||
url,
|
||||
static_url,
|
||||
};
|
||||
|
||||
if (category) {
|
||||
state.customCategories[category] ??= [];
|
||||
if (!state.customCategories[category].includes(shortcode)) {
|
||||
state.customCategories[category].push(shortcode);
|
||||
}
|
||||
}
|
||||
|
||||
state.customLoaded = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const emojis = emojisSlice.reducer;
|
||||
export const { loadLocale } = emojisSlice.actions;
|
||||
|
||||
export const loadCustomEmojis = createAsyncThunk(
|
||||
`${emojisSlice.name}/loadCustomEmojis`,
|
||||
async () => {
|
||||
const { loadAllCustomEmoji } =
|
||||
await import('@/mastodon/features/emoji/database');
|
||||
return loadAllCustomEmoji();
|
||||
},
|
||||
);
|
||||
@ -1,9 +1,11 @@
|
||||
import { annualReport } from './annual_report';
|
||||
import { collections } from './collections';
|
||||
import { emojis } from './emojis';
|
||||
import { profileEdit } from './profile_edit';
|
||||
|
||||
export const sliceReducers = {
|
||||
annualReport,
|
||||
collections,
|
||||
emojis,
|
||||
profileEdit,
|
||||
};
|
||||
|
||||
@ -3017,7 +3017,6 @@ a.account__display-name {
|
||||
|
||||
&__pane {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-width: 285px;
|
||||
|
||||
@ -33,6 +33,7 @@ class Collection < ApplicationRecord
|
||||
has_many :accepted_collection_items, -> { accepted }, class_name: 'CollectionItem', inverse_of: :collection # rubocop:disable Rails/HasManyOrHasOneDependent
|
||||
has_many :collection_reports, dependent: :delete_all
|
||||
has_many :accounts, -> { merge(CollectionItem.pending_or_accepted) }, through: :collection_items
|
||||
has_many :notifications, as: :activity, dependent: :destroy
|
||||
|
||||
validates :name, presence: true
|
||||
validates :name, length: { maximum: 40 }, if: :local?
|
||||
|
||||
@ -136,7 +136,7 @@ class Notification < ApplicationRecord
|
||||
belongs_to :generated_annual_report, inverse_of: false
|
||||
belongs_to :quote, inverse_of: :notification
|
||||
belongs_to :collection_item, inverse_of: false # TODO: have an inverse?
|
||||
belongs_to :collection, inverse_of: false # TODO: have an inverse?
|
||||
belongs_to :collection, inverse_of: :notifications
|
||||
end
|
||||
|
||||
validates :type, inclusion: { in: TYPES }
|
||||
|
||||
@ -31,6 +31,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||
else
|
||||
{
|
||||
url: frontend_asset_url('images/preview.png'),
|
||||
description: I18n.t('about.default_thumbnail_description', locale: object.languages[0]),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@ -4,6 +4,7 @@ en:
|
||||
about_mastodon_html: 'The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!'
|
||||
contact_missing: Not set
|
||||
contact_unavailable: N/A
|
||||
default_thumbnail_description: Two smiling cartoon mastodons (who look like elephants) toss a paper plane between them. They're surrounded by a bright blue sky and floating planets with trees and more mastodons on them.
|
||||
hosted_on: Mastodon hosted on %{domain}
|
||||
title: About
|
||||
accounts:
|
||||
|
||||
61
config/vite/plugin-sw-chunk-paths.ts
Normal file
61
config/vite/plugin-sw-chunk-paths.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/* The service worker is built as a regular Vite entry point and is served from
|
||||
`/sw.js` (a symlink to `packs/sw.js`), while the chunks it depends on live in
|
||||
the build output directory (e.g. `/packs/`).
|
||||
|
||||
Browsers resolve a module's static import specifiers relative to the *URL* of
|
||||
the script, not its location on disk. Because the worker is served from the
|
||||
root, relative specifiers such as `./chunk-abc123.js` resolve to
|
||||
`/chunk-abc123.js` and return a 404, which prevents the service worker from
|
||||
installing.
|
||||
|
||||
This plugin rewrites those relative specifiers in the service worker output
|
||||
to absolute paths pointing at the build output directory, so they resolve
|
||||
correctly regardless of the URL the worker is served from.
|
||||
*/
|
||||
|
||||
import type { Plugin, ResolvedConfig } from 'vite';
|
||||
|
||||
const SERVICE_WORKER_FILENAME = 'sw.js';
|
||||
|
||||
export function MastodonServiceWorkerChunkPaths(): Plugin {
|
||||
let config: ResolvedConfig;
|
||||
|
||||
return {
|
||||
name: 'mastodon-sw-chunk-paths',
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
},
|
||||
renderChunk(code, chunk) {
|
||||
if (chunk.fileName !== SERVICE_WORKER_FILENAME) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Resolve the base from the final Vite config rather than receiving it as
|
||||
// an argument, so forks or other plugins that alter `base` are respected.
|
||||
// Vite normalises `base` to always include a trailing slash.
|
||||
const { base } = config;
|
||||
|
||||
// Drive the rewrite from the chunk's actual dependency list rather than a
|
||||
// generic regex, so we only ever touch real (hashed) chunk specifiers and
|
||||
// never accidentally match similar-looking text inside string literals.
|
||||
const dependencies = [...chunk.imports, ...chunk.dynamicImports];
|
||||
|
||||
let rewritten = code;
|
||||
for (const dependency of dependencies) {
|
||||
// The worker sits at the root of the output directory, so each
|
||||
// dependency is imported with a leading `./`.
|
||||
rewritten = rewritten
|
||||
.replaceAll(`"./${dependency}"`, `"${base}${dependency}"`)
|
||||
.replaceAll(`'./${dependency}'`, `'${base}${dependency}'`);
|
||||
}
|
||||
|
||||
if (rewritten === code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only import specifiers are changed, so the existing source map stays
|
||||
// accurate enough; returning `null` avoids a spurious sourcemap warning.
|
||||
return { code: rewritten, map: null };
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -17,7 +17,7 @@ module Mastodon
|
||||
end
|
||||
|
||||
def default_prerelease
|
||||
'rc.1'
|
||||
'rc.2'
|
||||
end
|
||||
|
||||
def prerelease
|
||||
|
||||
@ -221,4 +221,19 @@ RSpec.describe Collection do
|
||||
expect(subject.to_log_permalink).to eq ActivityPub::TagManager.instance.uri_for(subject)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#destroy' do
|
||||
let(:collection) { Fabricate(:collection) }
|
||||
|
||||
before do
|
||||
Fabricate(:notification, activity: collection, type: :added_to_collection)
|
||||
Fabricate(:notification, activity: collection, type: :collection_update)
|
||||
end
|
||||
|
||||
it 'removes the collection and all notifications that reference it' do
|
||||
expect { collection.destroy }
|
||||
.to change(described_class, :count).by(-1)
|
||||
.and change(Notification, :count).by(-2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -22,6 +22,7 @@ import svgr from 'vite-plugin-svgr';
|
||||
import { MastodonAssetsManifest } from './config/vite/plugin-assets-manifest';
|
||||
import { GlitchThemes as MastodonThemes } from './config/vite/plugin-glitch-themes';
|
||||
import { MastodonNameLookup } from './config/vite/plugin-name-lookup';
|
||||
import { MastodonServiceWorkerChunkPaths } from './config/vite/plugin-sw-chunk-paths';
|
||||
import { MastodonServiceWorkerLocales } from './config/vite/plugin-sw-locales';
|
||||
|
||||
const jsRoot = path.resolve(__dirname, 'app/javascript');
|
||||
@ -185,6 +186,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
|
||||
MastodonThemes(),
|
||||
MastodonAssetsManifest(),
|
||||
MastodonServiceWorkerLocales(),
|
||||
MastodonServiceWorkerChunkPaths(),
|
||||
legacy({
|
||||
renderLegacyChunks: false,
|
||||
modernPolyfills: true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user