mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-24 19:37:26 +00:00
Merge commit '366856f3bcdc2ff008b04e493a5de317ab83d5d0' into glitch-soc/merge-upstream
This commit is contained in:
@@ -180,25 +180,24 @@ export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
|
||||
|
||||
if (shouldHandleEvent) {
|
||||
const matchCandidates: {
|
||||
handler: (event: KeyboardEvent) => void;
|
||||
// A candidate will be have an undefined handler if it's matched,
|
||||
// but handled in a parent component rather than this one.
|
||||
handler: ((event: KeyboardEvent) => void) | undefined;
|
||||
priority: number;
|
||||
}[] = [];
|
||||
|
||||
(Object.keys(hotkeyMatcherMap) as HotkeyName[]).forEach(
|
||||
(handlerName) => {
|
||||
const handler = handlersRef.current[handlerName];
|
||||
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
|
||||
|
||||
if (handler) {
|
||||
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
|
||||
const { isMatch, priority } = hotkeyMatcher(
|
||||
event,
|
||||
bufferedKeys.current,
|
||||
);
|
||||
|
||||
const { isMatch, priority } = hotkeyMatcher(
|
||||
event,
|
||||
bufferedKeys.current,
|
||||
);
|
||||
|
||||
if (isMatch) {
|
||||
matchCandidates.push({ handler, priority });
|
||||
}
|
||||
if (isMatch) {
|
||||
matchCandidates.push({ handler, priority });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
||||
return (
|
||||
<Link
|
||||
className={classNames('mention hashtag', className)}
|
||||
to={`/tags/${hashtag}`}
|
||||
to={`/tags/${encodeURIComponent(hashtag)}`}
|
||||
rel='tag'
|
||||
data-menu-hashtag={hashtagAccountId}
|
||||
>
|
||||
@@ -71,7 +71,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
href={encodeURI(href)}
|
||||
href={href}
|
||||
title={href}
|
||||
className={classNames('unhandled-link', className)}
|
||||
target='_blank'
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { pasteLinkCompose } from 'mastodon/actions/compose_typed';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { PRIVATE_QUOTE_MODAL_ID } from 'mastodon/features/ui/components/confirmation_modals/private_quote_notify';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
|
||||
import ComposeForm from '../components/compose_form';
|
||||
|
||||
@@ -53,6 +54,7 @@ const mapStateToProps = state => ({
|
||||
quoteToPrivate:
|
||||
!!state.getIn(['compose', 'quoted_status_id'])
|
||||
&& state.getIn(['compose', 'privacy']) === 'private'
|
||||
&& state.getIn(['statuses', state.getIn(['compose', 'quoted_status_id']), 'account']) !== me
|
||||
&& !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]),
|
||||
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
||||
lang: state.getIn(['compose', 'language']),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { initialState } from '@/mastodon/initial_state';
|
||||
|
||||
import { toSupportedLocale } from './locale';
|
||||
import type { LocaleOrCustom } from './types';
|
||||
import { emojiLogger } from './utils';
|
||||
// eslint-disable-next-line import/default -- Importing via worker loader.
|
||||
import EmojiWorker from './worker?worker&inline';
|
||||
@@ -24,19 +25,17 @@ export function initializeEmoji() {
|
||||
}
|
||||
|
||||
if (worker) {
|
||||
// Assign worker to const to make TS happy inside the event listener.
|
||||
const thisWorker = worker;
|
||||
const timeoutId = setTimeout(() => {
|
||||
log('worker is not ready after timeout');
|
||||
worker = null;
|
||||
void fallbackLoad();
|
||||
}, WORKER_TIMEOUT);
|
||||
thisWorker.addEventListener('message', (event: MessageEvent<string>) => {
|
||||
worker.addEventListener('message', (event: MessageEvent<string>) => {
|
||||
const { data: message } = event;
|
||||
if (message === 'ready') {
|
||||
log('worker ready, loading data');
|
||||
clearTimeout(timeoutId);
|
||||
thisWorker.postMessage('custom');
|
||||
messageWorker('custom');
|
||||
void loadEmojiLocale(userLocale);
|
||||
// Load English locale as well, because people are still used to
|
||||
// using it from before we supported other locales.
|
||||
@@ -55,20 +54,35 @@ export function initializeEmoji() {
|
||||
async function fallbackLoad() {
|
||||
log('falling back to main thread for loading');
|
||||
const { importCustomEmojiData } = await import('./loader');
|
||||
await importCustomEmojiData();
|
||||
const emojis = await importCustomEmojiData();
|
||||
if (emojis) {
|
||||
log('loaded %d custom emojis', emojis.length);
|
||||
}
|
||||
await loadEmojiLocale(userLocale);
|
||||
if (userLocale !== 'en') {
|
||||
await loadEmojiLocale('en');
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadEmojiLocale(localeString: string) {
|
||||
async function loadEmojiLocale(localeString: string) {
|
||||
const locale = toSupportedLocale(localeString);
|
||||
const { importEmojiData, localeToPath } = await import('./loader');
|
||||
|
||||
if (worker) {
|
||||
worker.postMessage(locale);
|
||||
const path = await localeToPath(locale);
|
||||
log('asking worker to load locale %s from %s', locale, path);
|
||||
messageWorker(locale, path);
|
||||
} else {
|
||||
const { importEmojiData } = await import('./loader');
|
||||
await importEmojiData(locale);
|
||||
const emojis = await importEmojiData(locale);
|
||||
if (emojis) {
|
||||
log('loaded %d emojis to locale %s', emojis.length, locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function messageWorker(locale: LocaleOrCustom, path?: string) {
|
||||
if (!worker) {
|
||||
return;
|
||||
}
|
||||
worker.postMessage({ locale, path });
|
||||
}
|
||||
|
||||
@@ -8,44 +8,64 @@ import {
|
||||
putLatestEtag,
|
||||
} from './database';
|
||||
import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale';
|
||||
import type { CustomEmojiData, LocaleOrCustom } from './types';
|
||||
import { emojiLogger } from './utils';
|
||||
import type { CustomEmojiData } from './types';
|
||||
|
||||
const log = emojiLogger('loader');
|
||||
|
||||
export async function importEmojiData(localeString: string) {
|
||||
export async function importEmojiData(localeString: string, path?: string) {
|
||||
const locale = toSupportedLocale(localeString);
|
||||
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale);
|
||||
|
||||
// Validate the provided path.
|
||||
if (path && !/^[/a-z]*\/packs\/assets\/compact-\w+\.json$/.test(path)) {
|
||||
throw new Error('Invalid path for emoji data');
|
||||
} else {
|
||||
// Otherwise get the path if not provided.
|
||||
path ??= await localeToPath(locale);
|
||||
}
|
||||
|
||||
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale, path);
|
||||
if (!emojis) {
|
||||
return;
|
||||
}
|
||||
const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis);
|
||||
log('loaded %d for %s locale', flattenedEmojis.length, locale);
|
||||
await putEmojiData(flattenedEmojis, locale);
|
||||
return flattenedEmojis;
|
||||
}
|
||||
|
||||
export async function importCustomEmojiData() {
|
||||
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>('custom');
|
||||
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>(
|
||||
'custom',
|
||||
'/api/v1/custom_emojis',
|
||||
);
|
||||
if (!emojis) {
|
||||
return;
|
||||
}
|
||||
log('loaded %d custom emojis', emojis.length);
|
||||
await putCustomEmojiData(emojis);
|
||||
return emojis;
|
||||
}
|
||||
|
||||
async function fetchAndCheckEtag<ResultType extends object[]>(
|
||||
localeOrCustom: LocaleOrCustom,
|
||||
const modules = import.meta.glob<string>(
|
||||
'../../../../../node_modules/emojibase-data/**/compact.json',
|
||||
{
|
||||
query: '?url',
|
||||
import: 'default',
|
||||
},
|
||||
);
|
||||
|
||||
export function localeToPath(locale: Locale) {
|
||||
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
|
||||
if (!modules[key] || typeof modules[key] !== 'function') {
|
||||
throw new Error(`Unsupported locale: ${locale}`);
|
||||
}
|
||||
return modules[key]();
|
||||
}
|
||||
|
||||
export async function fetchAndCheckEtag<ResultType extends object[]>(
|
||||
localeString: string,
|
||||
path: string,
|
||||
): Promise<ResultType | null> {
|
||||
const locale = toSupportedLocaleOrCustom(localeOrCustom);
|
||||
const locale = toSupportedLocaleOrCustom(localeString);
|
||||
|
||||
// Use location.origin as this script may be loaded from a CDN domain.
|
||||
const url = new URL(location.origin);
|
||||
if (locale === 'custom') {
|
||||
url.pathname = '/api/v1/custom_emojis';
|
||||
} else {
|
||||
const modulePath = await localeToPath(locale);
|
||||
url.pathname = modulePath;
|
||||
}
|
||||
const url = new URL(path, location.origin);
|
||||
|
||||
const oldEtag = await loadLatestEtag(locale);
|
||||
const response = await fetch(url, {
|
||||
@@ -60,38 +80,20 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch emoji data for ${localeOrCustom}: ${response.statusText}`,
|
||||
`Failed to fetch emoji data for ${locale}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ResultType;
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error(
|
||||
`Unexpected data format for ${localeOrCustom}: expected an array`,
|
||||
);
|
||||
throw new Error(`Unexpected data format for ${locale}: expected an array`);
|
||||
}
|
||||
|
||||
// Store the ETag for future requests
|
||||
const etag = response.headers.get('ETag');
|
||||
if (etag) {
|
||||
await putLatestEtag(etag, localeOrCustom);
|
||||
await putLatestEtag(etag, localeString);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const modules = import.meta.glob<string>(
|
||||
'../../../../../node_modules/emojibase-data/**/compact.json',
|
||||
{
|
||||
query: '?url',
|
||||
import: 'default',
|
||||
},
|
||||
);
|
||||
|
||||
function localeToPath(locale: Locale) {
|
||||
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
|
||||
if (!modules[key] || typeof modules[key] !== 'function') {
|
||||
throw new Error(`Unsupported locale: ${locale}`);
|
||||
}
|
||||
return modules[key]();
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ describe('loadEmojiDataToState', () => {
|
||||
const dbCall = vi
|
||||
.spyOn(db, 'loadEmojiByHexcode')
|
||||
.mockRejectedValue(new db.LocaleNotLoadedError('en'));
|
||||
vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce();
|
||||
vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce(undefined);
|
||||
const consoleCall = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementationOnce(() => null);
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { importEmojiData, importCustomEmojiData } from './loader';
|
||||
import { importCustomEmojiData, importEmojiData } from './loader';
|
||||
|
||||
addEventListener('message', handleMessage);
|
||||
self.postMessage('ready'); // After the worker is ready, notify the main thread
|
||||
|
||||
function handleMessage(event: MessageEvent<string>) {
|
||||
const { data: locale } = event;
|
||||
void loadData(locale);
|
||||
function handleMessage(event: MessageEvent<{ locale: string; path?: string }>) {
|
||||
const {
|
||||
data: { locale, path },
|
||||
} = event;
|
||||
void loadData(locale, path);
|
||||
}
|
||||
|
||||
async function loadData(locale: string) {
|
||||
if (locale !== 'custom') {
|
||||
await importEmojiData(locale);
|
||||
async function loadData(locale: string, path?: string) {
|
||||
let importCount: number | undefined;
|
||||
if (locale === 'custom') {
|
||||
importCount = (await importCustomEmojiData())?.length;
|
||||
} else if (path) {
|
||||
importCount = (await importEmojiData(locale, path))?.length;
|
||||
} else {
|
||||
await importCustomEmojiData();
|
||||
throw new Error('Path is required for loading locale emoji data');
|
||||
}
|
||||
if (importCount) {
|
||||
self.postMessage(`loaded ${importCount} emojis into ${locale}`);
|
||||
}
|
||||
self.postMessage(`loaded ${locale}`);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
@@ -55,6 +55,8 @@ export const DetailedStatus: React.FC<{
|
||||
pictureInPicture: any;
|
||||
onToggleHidden?: (status: any) => void;
|
||||
onToggleMediaVisibility?: () => void;
|
||||
ancestors?: number;
|
||||
multiColumn?: boolean;
|
||||
}> = ({
|
||||
status,
|
||||
onOpenMedia,
|
||||
@@ -69,6 +71,8 @@ export const DetailedStatus: React.FC<{
|
||||
pictureInPicture,
|
||||
onToggleMediaVisibility,
|
||||
onToggleHidden,
|
||||
ancestors = 0,
|
||||
multiColumn = false,
|
||||
}) => {
|
||||
const properStatus = status?.get('reblog') ?? status;
|
||||
const [height, setHeight] = useState(0);
|
||||
@@ -123,6 +127,30 @@ export const DetailedStatus: React.FC<{
|
||||
if (onTranslate) onTranslate(status);
|
||||
}, [onTranslate, status]);
|
||||
|
||||
// The component is managed and will change if the status changes
|
||||
// Ancestors can increase when loading a thread, in which case we want to scroll,
|
||||
// or decrease if a post is deleted, in which case we don't want to mess with it
|
||||
const previousAncestors = useRef(-1);
|
||||
useEffect(() => {
|
||||
if (nodeRef.current && previousAncestors.current < ancestors) {
|
||||
nodeRef.current.scrollIntoView(true);
|
||||
|
||||
// In the single-column interface, `scrollIntoView` will put the post behind the header, so compensate for that.
|
||||
if (!multiColumn) {
|
||||
const offset = document
|
||||
.querySelector('.column-header__wrapper')
|
||||
?.getBoundingClientRect().bottom;
|
||||
|
||||
if (offset) {
|
||||
const scrollingElement = document.scrollingElement ?? document.body;
|
||||
scrollingElement.scrollBy(0, -offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousAncestors.current = ancestors;
|
||||
}, [ancestors, multiColumn]);
|
||||
|
||||
if (!properStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -164,8 +164,6 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
componentDidMount () {
|
||||
attachFullscreenListener(this.onFullScreenChange);
|
||||
|
||||
this._scrollStatusIntoView();
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
@@ -487,35 +485,11 @@ class Status extends ImmutablePureComponent {
|
||||
this.statusNode = c;
|
||||
};
|
||||
|
||||
_scrollStatusIntoView () {
|
||||
const { status, multiColumn } = this.props;
|
||||
|
||||
if (status) {
|
||||
requestIdleCallback(() => {
|
||||
this.statusNode?.scrollIntoView(true);
|
||||
|
||||
// In the single-column interface, `scrollIntoView` will put the post behind the header,
|
||||
// so compensate for that.
|
||||
if (!multiColumn) {
|
||||
const offset = document.querySelector('.column-header__wrapper')?.getBoundingClientRect()?.bottom;
|
||||
if (offset) {
|
||||
const scrollingElement = document.scrollingElement || document.body;
|
||||
scrollingElement.scrollBy(0, -offset);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
const { status, descendantsIds } = this.props;
|
||||
|
||||
const isSameStatus = status && (prevProps.status?.get('id') === status.get('id'));
|
||||
|
||||
if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || !isSameStatus)) {
|
||||
this._scrollStatusIntoView();
|
||||
}
|
||||
|
||||
// Only highlight replies after the initial load
|
||||
if (prevProps.descendantsIds.length && isSameStatus) {
|
||||
const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds);
|
||||
@@ -619,6 +593,8 @@ class Status extends ImmutablePureComponent {
|
||||
showMedia={this.state.showMedia}
|
||||
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
||||
pictureInPicture={pictureInPicture}
|
||||
ancestors={this.props.ancestorsIds.length}
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
<ActionBar
|
||||
|
||||
@@ -903,6 +903,7 @@
|
||||
"status.edited_x_times": "Upraveno {count, plural, one {{count}krát} few {{count}krát} many {{count}krát} other {{count}krát}}",
|
||||
"status.embed": "Získejte kód pro vložení",
|
||||
"status.favourite": "Oblíbit",
|
||||
"status.favourites_count": "{count, plural, one {{counter} oblíbený} few {{counter} oblíbené} many {{counter} oblíbených} other {{counter} oblíbených}}",
|
||||
"status.filter": "Filtrovat tento příspěvek",
|
||||
"status.history.created": "Uživatel {name} vytvořil {date}",
|
||||
"status.history.edited": "Uživatel {name} upravil {date}",
|
||||
@@ -937,12 +938,14 @@
|
||||
"status.quotes.empty": "Tento příspěvek zatím nikdo necitoval. Pokud tak někdo učiní, uvidíte to zde.",
|
||||
"status.quotes.local_other_disclaimer": "Citace zamítnuté autorem nebudou zobrazeny.",
|
||||
"status.quotes.remote_other_disclaimer": "Pouze citace z {domain} zde budou zaručeně ukázány. Citace zamítnuté autorem nebudou zobrazeny.",
|
||||
"status.quotes_count": "{count, plural, one {{counter} citace} few {{counter} citace} many {{counter} citací} other {{counter} citací}}",
|
||||
"status.read_more": "Číst více",
|
||||
"status.reblog": "Boostnout",
|
||||
"status.reblog_or_quote": "Boostnout nebo citovat",
|
||||
"status.reblog_private": "Sdílejte znovu se svými sledujícími",
|
||||
"status.reblogged_by": "Uživatel {name} boostnul",
|
||||
"status.reblogs.empty": "Tento příspěvek ještě nikdo neboostnul. Pokud to někdo udělá, zobrazí se zde.",
|
||||
"status.reblogs_count": "{count, plural, one {{counter} boost} few {{counter} boosty} many {{counter} boostů} other {{counter} boostů}}",
|
||||
"status.redraft": "Smazat a přepsat",
|
||||
"status.remove_bookmark": "Odstranit ze záložek",
|
||||
"status.remove_favourite": "Odebrat z oblíbených",
|
||||
|
||||
@@ -903,6 +903,7 @@
|
||||
"status.edited_x_times": "Golygwyd {count, plural, one {{count} gwaith} other {{count} gwaith}}",
|
||||
"status.embed": "Cael y cod mewnblannu",
|
||||
"status.favourite": "Ffafrio",
|
||||
"status.favourites_count": "{count, plural, one {{counter} ffefryn} other {{counter} ffefryn}}",
|
||||
"status.filter": "Hidlo'r postiad hwn",
|
||||
"status.history.created": "Crëwyd gan {name} {date}",
|
||||
"status.history.edited": "Golygwyd gan {name} {date}",
|
||||
@@ -937,12 +938,14 @@
|
||||
"status.quotes.empty": "Does neb wedi dyfynnu'r postiad hwn eto. Pan fydd rhywun yn gwneud hynny, bydd yn ymddangos yma.",
|
||||
"status.quotes.local_other_disclaimer": "Bydd dyfyniadau wedi'u gwrthod gan yr awdur ddim yn cael eu dangos.",
|
||||
"status.quotes.remote_other_disclaimer": "Dim ond dyfyniadau o {domain} sy'n siŵr o gael eu dangos yma. Bydd dyfyniadau wedi'u gwrthod gan yr awdur ddim yn cael eu dangos.",
|
||||
"status.quotes_count": "{count, plural, one {{counter} dyfyniad} other {{counter} dyfyniad}}",
|
||||
"status.read_more": "Darllen rhagor",
|
||||
"status.reblog": "Hybu",
|
||||
"status.reblog_or_quote": "Hybu neu ddyfynnu",
|
||||
"status.reblog_private": "Rhannwch eto gyda'ch dilynwyr",
|
||||
"status.reblogged_by": "Hybodd {name}",
|
||||
"status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.",
|
||||
"status.reblogs_count": "{count, plural, one {{counter} hwb} other {{counter} hwb}}",
|
||||
"status.redraft": "Dileu ac ail lunio",
|
||||
"status.remove_bookmark": "Tynnu nod tudalen",
|
||||
"status.remove_favourite": "Tynnu o'r ffefrynnau",
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
"confirmations.delete_list.title": "Slet liste?",
|
||||
"confirmations.discard_draft.confirm": "Kassér og fortsæt",
|
||||
"confirmations.discard_draft.edit.cancel": "Fortsæt redigering",
|
||||
"confirmations.discard_draft.edit.message": "Hvis du fortsætter, kasseres alle ændringer, du har foretaget i det indlæg, du er i gang med at redigere.",
|
||||
"confirmations.discard_draft.edit.message": "Hvis du fortsætter, vil alle ændringer, du har foretaget i det indlæg, du er er ved at redigere, blive slettet.",
|
||||
"confirmations.discard_draft.edit.title": "Kassér ændringer til dit indlæg?",
|
||||
"confirmations.discard_draft.post.cancel": "Genoptag udkast",
|
||||
"confirmations.discard_draft.post.message": "Hvis du fortsætter, kasseres det indlæg, du er i gang med at udforme.",
|
||||
@@ -507,7 +507,7 @@
|
||||
"keyboard_shortcuts.pinned": "Åbn liste over fastgjorte indlæg",
|
||||
"keyboard_shortcuts.profile": "Åbn forfatters profil",
|
||||
"keyboard_shortcuts.quote": "Citér indlæg",
|
||||
"keyboard_shortcuts.reply": "Besvar indlægget",
|
||||
"keyboard_shortcuts.reply": "Besvar indlæg",
|
||||
"keyboard_shortcuts.requests": "Åbn liste over følgeanmodninger",
|
||||
"keyboard_shortcuts.search": "Fokusér søgebjælke",
|
||||
"keyboard_shortcuts.spoilers": "Vis/skjul indholdsadvarsel-felt",
|
||||
@@ -675,7 +675,7 @@
|
||||
"notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke",
|
||||
"notifications.column_settings.follow": "Nye følgere:",
|
||||
"notifications.column_settings.follow_request": "Nye følgeanmodninger:",
|
||||
"notifications.column_settings.group": "Gruppere",
|
||||
"notifications.column_settings.group": "Gruppér",
|
||||
"notifications.column_settings.mention": "Omtaler:",
|
||||
"notifications.column_settings.poll": "Afstemningsresultater:",
|
||||
"notifications.column_settings.push": "Push-notifikationer",
|
||||
@@ -764,7 +764,7 @@
|
||||
"privacy_policy.last_updated": "Senest opdateret {date}",
|
||||
"privacy_policy.title": "Privatlivspolitik",
|
||||
"quote_error.edit": "Citater kan ikke tilføjes ved redigering af et indlæg.",
|
||||
"quote_error.poll": "Citering ikke tilladt i afstemninger.",
|
||||
"quote_error.poll": "Citering er ikke tilladt med afstemninger.",
|
||||
"quote_error.private_mentions": "Citering er ikke tilladt med direkte omtaler.",
|
||||
"quote_error.quote": "Kun ét citat ad gangen er tilladt.",
|
||||
"quote_error.unauthorized": "Du har ikke tilladelse til at citere dette indlæg.",
|
||||
@@ -867,7 +867,7 @@
|
||||
"search_results.title": "Søg efter \"{q}\"",
|
||||
"server_banner.about_active_users": "Personer, som brugte denne server de seneste 30 dage (månedlige aktive brugere)",
|
||||
"server_banner.active_users": "aktive brugere",
|
||||
"server_banner.administered_by": "Håndteres af:",
|
||||
"server_banner.administered_by": "Administreret af:",
|
||||
"server_banner.is_one_of_many": "{domain} er en af de mange uafhængige Mastodon-servere, du kan bruge for at deltage i fediverset.",
|
||||
"server_banner.server_stats": "Serverstatstik:",
|
||||
"sign_in_banner.create_account": "Opret konto",
|
||||
@@ -902,7 +902,7 @@
|
||||
"status.edited": "Senest redigeret {date}",
|
||||
"status.edited_x_times": "Redigeret {count, plural, one {{count} gang} other {{count} gange}}",
|
||||
"status.embed": "Hent indlejringskode",
|
||||
"status.favourite": "Favorit",
|
||||
"status.favourite": "Favoritmarkér",
|
||||
"status.favourites_count": "{count, plural, one {{counter} favorit} other {{counter} favoritter}}",
|
||||
"status.filter": "Filtrér dette indlæg",
|
||||
"status.history.created": "{name} oprettet {date}",
|
||||
@@ -991,7 +991,7 @@
|
||||
"units.short.million": "{count} mio.",
|
||||
"units.short.thousand": "{count} tusind",
|
||||
"upload_area.title": "Træk og slip for at uploade",
|
||||
"upload_button.label": "Tilføj billed-, video- eller lydfil(er)",
|
||||
"upload_button.label": "Tilføj billeder, en video- eller lydfil",
|
||||
"upload_error.limit": "Grænse for filupload nået.",
|
||||
"upload_error.poll": "Filupload ikke tilladt for afstemninger.",
|
||||
"upload_error.quote": "Fil-upload ikke tilladt i citater.",
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
"confirmations.follow_to_list.message": "Du musst {name} folgen, um das Profil zu einer Liste hinzufügen zu können.",
|
||||
"confirmations.follow_to_list.title": "Profil folgen?",
|
||||
"confirmations.logout.confirm": "Abmelden",
|
||||
"confirmations.logout.message": "Möchtest du dich wirklich abmelden?",
|
||||
"confirmations.logout.message": "Bist du sicher, dass du dich abmelden möchtest?",
|
||||
"confirmations.logout.title": "Abmelden?",
|
||||
"confirmations.missing_alt_text.confirm": "Bildbeschreibung hinzufügen",
|
||||
"confirmations.missing_alt_text.message": "Dein Beitrag enthält Medien ohne Bildbeschreibung. Mit ALT-Texten erreichst Du auch Menschen, die blind oder sehbehindert sind.",
|
||||
@@ -584,8 +584,8 @@
|
||||
"navigation_bar.follows_and_followers": "Follower und Folge ich",
|
||||
"navigation_bar.import_export": "Importieren und exportieren",
|
||||
"navigation_bar.lists": "Listen",
|
||||
"navigation_bar.live_feed_local": "Live-Feed (lokal)",
|
||||
"navigation_bar.live_feed_public": "Live-Feed (öffentlich)",
|
||||
"navigation_bar.live_feed_local": "Live-Feed (Dieser Server)",
|
||||
"navigation_bar.live_feed_public": "Live-Feed (Alle Server)",
|
||||
"navigation_bar.logout": "Abmelden",
|
||||
"navigation_bar.moderation": "Moderation",
|
||||
"navigation_bar.more": "Mehr",
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
"confirmations.discard_draft.edit.cancel": "Palaa muokkaamaan",
|
||||
"confirmations.discard_draft.edit.message": "Jatkaminen tuhoaa kaikki muutokset, joita olet tehnyt julkaisuun, jota olet parhaillaan muokkaamassa.",
|
||||
"confirmations.discard_draft.edit.title": "Hylätäänkö luonnosjulkaisusi muutokset?",
|
||||
"confirmations.discard_draft.post.cancel": "Palaa lunnokseen",
|
||||
"confirmations.discard_draft.post.cancel": "Palaa luonnokseen",
|
||||
"confirmations.discard_draft.post.message": "Jatkaminen tuhoaa julkaisun, jota olet parhaillaan laatimassa.",
|
||||
"confirmations.discard_draft.post.title": "Hylätäänkö luonnosjulkaisusi?",
|
||||
"confirmations.discard_edit_media.confirm": "Hylkää",
|
||||
|
||||
@@ -528,7 +528,7 @@
|
||||
"limited_account_hint.action": "Afficher le profil quand même",
|
||||
"limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
|
||||
"link_preview.author": "Par {name}",
|
||||
"link_preview.more_from_author": "Plus via {name}",
|
||||
"link_preview.more_from_author": "Voir plus de {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",
|
||||
"lists.add_member": "Ajouter",
|
||||
"lists.add_to_list": "Ajouter à la liste",
|
||||
|
||||
@@ -528,7 +528,7 @@
|
||||
"limited_account_hint.action": "Afficher le profil quand même",
|
||||
"limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
|
||||
"link_preview.author": "Par {name}",
|
||||
"link_preview.more_from_author": "Plus via {name}",
|
||||
"link_preview.more_from_author": "Voir plus de {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",
|
||||
"lists.add_member": "Ajouter",
|
||||
"lists.add_to_list": "Ajouter à la liste",
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
"notification.update": "{name} nuntium correxit",
|
||||
"notification_requests.accept": "Accipe",
|
||||
"notification_requests.confirm_accept_multiple.message": "Tu es accepturus {count, plural, one {una notitia petitionem} other {# notitia petitiones}}. Certus esne procedere vis?",
|
||||
"notification_requests.confirm_dismiss_multiple.message": "Tu {count, plural, one {unam petitionem notificationis} other {# petitiones notificationum}} abrogāre prōximum es. {count, plural, one {Illa} other {Eae}} facile accessū nōn erit. Certus es tē procedere velle?",
|
||||
"notification_requests.confirm_dismiss_multiple.message": "Tu {count, plural, one {unam petitionem notificationis} other {# petitiones notificationum}} abrogāre prōximum es. {count, plural, one {it} other {Eae}} facile accessū nōn erit. Certus es tē procedere velle?",
|
||||
"notifications.filter.all": "Omnia",
|
||||
"notifications.filter.polls": "Eventus electionis",
|
||||
"notifications.group": "{count} Notificātiōnēs",
|
||||
@@ -246,19 +246,102 @@
|
||||
"status.history.created": "{name} creatum {date}",
|
||||
"status.history.edited": "{name} correxit {date}",
|
||||
"status.open": "Expand this status",
|
||||
"status.quotes.empty": "Nemo hanc commentationem adhuc citavit. Cum quis citaverit, hic apparebit.",
|
||||
"status.quotes.local_other_disclaimer": "Citationes ab auctore reiectæ non monstrabuntur.",
|
||||
"status.quotes.remote_other_disclaimer": "Tantum citae ex {domain} hic exhiberi praestantur. Citae ab auctore reiectae non exhibebuntur.",
|
||||
"status.quotes_count": "{count, plural, one {{counter} citatio} other {{counter} citationes}}",
|
||||
"status.read_more": "Plura lege",
|
||||
"status.reblog": "Promovere",
|
||||
"status.reblog_or_quote": "Promovere aut cita",
|
||||
"status.reblog_private": "Iterum cum sectatoribus tuis communica",
|
||||
"status.reblogged_by": "{name} adiuvavit",
|
||||
"status.reblogs.empty": "Nemo hanc publicationem adhuc promovit. Cum quis eam promoveat, hic apparebunt.",
|
||||
"status.reblogs_count": "{count, plural, one {{counter} incrementum} other {{counter} incrementa}}",
|
||||
"status.redraft": "Dele et redig",
|
||||
"status.remove_bookmark": "Tolle signum",
|
||||
"status.remove_favourite": "Tolle ad delectis",
|
||||
"status.remove_quote": "Tolle",
|
||||
"status.replied_in_thread": "In filo responsum",
|
||||
"status.replied_to": "{name} respondit",
|
||||
"status.reply": "Respondere",
|
||||
"status.replyAll": "Responde ad filum",
|
||||
"status.report": "Referre @{name}",
|
||||
"status.request_quote": "Pretium petere",
|
||||
"status.revoke_quote": "Tolle nuntium meum ex nuntio @{name}",
|
||||
"status.sensitive_warning": "Materia delicata",
|
||||
"status.share": "Communica",
|
||||
"status.show_less_all": "Omnibus minus monstra",
|
||||
"status.show_more_all": "Omnibus plura monstra",
|
||||
"status.show_original": "Monstra originalem",
|
||||
"status.title.with_attachments": "{user} publicavit {attachmentCount, plural, one {unum annexum} other {{attachmentCount} annexa}}",
|
||||
"status.translate": "Converte",
|
||||
"status.translated_from_with": "Translatum ex {lang} per {provider}",
|
||||
"status.uncached_media_warning": "Praevisum non praesto est",
|
||||
"status.unmute_conversation": "Conversationem reserare",
|
||||
"subscribed_languages.lead": "Tantum epistolae in linguis selectis in domo tua apparebunt et indices temporum post mutationem. Neminem eligatis qui epistolas in omnibus linguis recipiat.",
|
||||
"subscribed_languages.save": "Servare mutationes",
|
||||
"subscribed_languages.target": "Muta linguas subscriptas pro {target}",
|
||||
"tabs_bar.home": "Domi",
|
||||
"tabs_bar.menu": "Elenchus",
|
||||
"tabs_bar.notifications": "Acta Vicimediorum",
|
||||
"tabs_bar.publish": "Nova publicatio",
|
||||
"tabs_bar.search": "Quaere",
|
||||
"terms_of_service.effective_as_of": "Valet ex {date}",
|
||||
"terms_of_service.title": "Termini servitii",
|
||||
"terms_of_service.upcoming_changes_on": "Mutationes venturae die {date}",
|
||||
"time_remaining.days": "{number, plural, one {# die} other {# dies}} restant",
|
||||
"time_remaining.hours": "{number, plural, one {# hora} other {# horae}} restant",
|
||||
"time_remaining.minutes": "{number, plural, one {# minutum} other {# minuta}} restant",
|
||||
"time_remaining.moments": "Momenta reliqua",
|
||||
"time_remaining.seconds": "{number, plural, one {# secundum} other {# secunda}} restant",
|
||||
"trends.counter_by_accounts": "{count, plural, one {{counter} persōna} other {{counter} persōnae}} in {days, plural, one {diē prīdiē} other {diēbus praeteritīs {days}}}",
|
||||
"trends.counter_by_accounts": "{count, plural, one {{counter} persōna} other {{counter} persōnae}} in {days, plural, one {days} other {diēbus praeteritīs {days}}}",
|
||||
"trends.trending_now": "Nunc in usu",
|
||||
"ui.beforeunload": "Si Mastodon discesseris, tua epitome peribit.",
|
||||
"units.short.billion": "{count} millia milionum",
|
||||
"units.short.million": "{count} milionum",
|
||||
"units.short.thousand": "{count} millia",
|
||||
"upload_button.label": "Imaginēs, vīdeō aut fīle audītūs adde",
|
||||
"upload_area.title": "Trahe et depone ad imponendum",
|
||||
"upload_button.label": "Adde imagines, pelliculam, aut fasciculum sonorum.",
|
||||
"upload_error.limit": "Limes onerationis superatus est.",
|
||||
"upload_error.poll": "Nullis suffragiis licet fascicula imponere.",
|
||||
"upload_error.quote": "Nullum oneramentum fasciculi cum citationibus permittitur.",
|
||||
"upload_form.drag_and_drop.instructions": "Ad annexum mediorum tollendum, preme clavem \"Space\" aut \"Enter\". Dum traheis, utere clavibus sagittariis ad annexum mediorum in quamlibet partem movendum. Preme iterum \"Space\" aut \"Enter\" ad annexum mediorum in novo loco deponendum, aut preme \"Escape\" ad desinendum.",
|
||||
"upload_form.drag_and_drop.on_drag_cancel": "Tractatio revocata est. Adiunctum medium {item} demissum est.",
|
||||
"upload_form.drag_and_drop.on_drag_end": "Adhaesum medium {item} demissum est.",
|
||||
"upload_form.drag_and_drop.on_drag_over": "Adhaesum medium {item} motum est.",
|
||||
"upload_form.drag_and_drop.on_drag_start": "Adhaesum medium {item} sublatum est.",
|
||||
"upload_form.edit": "Recolere",
|
||||
"upload_progress.label": "Uploading…"
|
||||
"upload_progress.label": "Oneratur...",
|
||||
"upload_progress.processing": "Processus…",
|
||||
"username.taken": "Illud nomen usoris occupatum est. Aliud tenta.",
|
||||
"video.close": "Pelliculam claude",
|
||||
"video.download": "Prehendere fasciculus",
|
||||
"video.exit_fullscreen": "Exitus ex plenum monitorium",
|
||||
"video.expand": "Expande pelliculam",
|
||||
"video.fullscreen": "Plenum monitorium",
|
||||
"video.hide": "celare pellicula",
|
||||
"video.mute": "Mutus",
|
||||
"video.pause": "intermittere",
|
||||
"video.play": "gignere",
|
||||
"video.skip_backward": "Redire",
|
||||
"video.skip_forward": "Progredi",
|
||||
"video.unmute": "Sordes tollere",
|
||||
"video.volume_down": "Volumen deminui",
|
||||
"video.volume_up": "Volumen augete",
|
||||
"visibility_modal.button_title": "Visibilitatem statuere",
|
||||
"visibility_modal.direct_quote_warning.text": "Si praesentia configuramenta servaveris, sententia inserta in nexum convertetur.",
|
||||
"visibility_modal.direct_quote_warning.title": "Citatio in mentitionibus privatis inseriri non possunt.",
|
||||
"visibility_modal.header": "Visibilitas et interactio",
|
||||
"visibility_modal.helper.direct_quoting": "Mentiones privatae in Mastodon scriptae ab aliis citari non possunt.",
|
||||
"visibility_modal.helper.privacy_editing": "Visibilitas mutari non potest postquam nuntius publicatus est.",
|
||||
"visibility_modal.helper.privacy_private_self_quote": "Citationes propriae nuntiorum privatorum publicari non possunt.",
|
||||
"visibility_modal.helper.private_quoting": "Nuntii in Mastodon a sequacibus tantum scriptī ab aliis citari non possunt.",
|
||||
"visibility_modal.helper.unlisted_quoting": "Cum te citant, eorum scriptum etiam ex indicibus popularibus celabitur.",
|
||||
"visibility_modal.instructions": "Régula quis cum hoc scripto agere possit. Potes etiam ordinationes omnibus scriptis futuris adhibere, navigando ad <link>Praeferentias > Regulas scriptionis praedefinitas</link>.",
|
||||
"visibility_modal.privacy_label": "Visibilitas",
|
||||
"visibility_modal.quote_followers": "Sectatores tantum",
|
||||
"visibility_modal.quote_label": "Quis citare potest",
|
||||
"visibility_modal.quote_nobody": "Sicut me",
|
||||
"visibility_modal.quote_public": "quisquis",
|
||||
"visibility_modal.save": "Servare"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { me, reduceMotion } from 'mastodon/initial_state';
|
||||
import ready from 'mastodon/ready';
|
||||
import { store } from 'mastodon/store';
|
||||
|
||||
import { initializeEmoji } from './features/emoji';
|
||||
import { isProduction, isDevelopment } from './utils/environment';
|
||||
|
||||
function main() {
|
||||
@@ -30,6 +29,7 @@ function main() {
|
||||
});
|
||||
}
|
||||
|
||||
const { initializeEmoji } = await import('./features/emoji/index');
|
||||
initializeEmoji();
|
||||
|
||||
const root = createRoot(mountNode);
|
||||
|
||||
@@ -341,8 +341,8 @@ export const composeReducer = (state = initialState, action) => {
|
||||
const isDirect = state.get('privacy') === 'direct';
|
||||
return state
|
||||
.set('quoted_status_id', isDirect ? null : status.get('id'))
|
||||
.set('spoiler', status.get('sensitive'))
|
||||
.set('spoiler_text', status.get('spoiler_text'))
|
||||
.update('spoiler', spoiler => (spoiler) || !!status.get('spoiler_text'))
|
||||
.update('spoiler_text', (spoiler_text) => spoiler_text || status.get('spoiler_text'))
|
||||
.update('privacy', (visibility) => {
|
||||
if (['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private') {
|
||||
return 'private';
|
||||
|
||||
@@ -327,9 +327,9 @@ $content-width: 840px;
|
||||
font-weight: 700;
|
||||
color: $primary-text-color;
|
||||
text-transform: none;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
border-top: 0;
|
||||
|
||||
.comment {
|
||||
display: block;
|
||||
|
||||
@@ -319,9 +319,9 @@ $content-width: 840px;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
text-transform: none;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
border-top: 0;
|
||||
|
||||
.comment {
|
||||
display: block;
|
||||
|
||||
Reference in New Issue
Block a user