{
@@ -273,47 +583,74 @@ export const TextEditor = forwardRef(
e.preventDefault();
e.stopPropagation();
closeEmoji();
+ textareaRef.current?.focus();
+ return;
}
- }}
- onKeyDown={(e) => {
- // Redirect printable characters to the search input when it
- // doesn't already have focus (e.g. while navigating with arrows)
+ const search = emojiSearchRef.current;
+ if (e.key.startsWith("Arrow")) arrowedRef.current = true;
+ // Enter takes the emoji arrowed to, or else the best match —
+ // not the first result frimousse highlights on its own.
+ if (e.key === "Enter" && !arrowedRef.current) {
+ e.preventDefault();
+ e.stopPropagation();
+ firstEmoji()?.click();
+ return;
+ }
+ const printable = e.key.length === 1 && !e.ctrlKey &&
+ !e.metaKey && !e.altKey;
if (
- e.key.length === 1 &&
- !e.ctrlKey && !e.metaKey && !e.altKey &&
- document.activeElement !== emojiSearchRef.current
- ) {
- const search = emojiSearchRef.current;
- if (search) {
- e.preventDefault();
- search.focus({ preventScroll: true });
- // Inject the character via the native setter so React's
- // synthetic onChange fires and frimousse updates its search state
- const setter = Object.getOwnPropertyDescriptor(
- globalThis.HTMLInputElement.prototype,
- "value",
- )?.set;
- setter?.call(search, search.value + e.key);
- search.dispatchEvent(new Event("input", { bubbles: true }));
- }
+ (!printable && e.key !== "Backspace") ||
+ document.activeElement === search
+ ) return;
+ e.preventDefault();
+ if (emojiMode === "inline") {
+ // Typing while arrowing through the grid goes on with the
+ // `:query` in the textarea, which the search follows.
+ const el = textareaRef.current;
+ if (!el) return;
+ el.focus({ preventScroll: true });
+ document.execCommand(
+ printable ? "insertText" : "delete",
+ false,
+ printable ? e.key : undefined,
+ );
+ } else if (search) {
+ search.focus({ preventScroll: true });
+ setEmojiQuery(
+ printable ? search.value + e.key : search.value.slice(0, -1),
+ );
}
}}
>
handleEmojiSelect(e.emoji)}
+ onEmojiSelect={(e) => selectEmoji(e.emoji)}
locale={i18n.locale as EmojiLocale}
+ skinTone={readSkinTone()}
>
+
setEmojiQuery(e.target.value)}
+ placeholder={t`Search emoji…`}
+ aria-label={t`Search emoji`}
+ // Inline, the query is typed in the textarea; the field
+ // only shows it.
+ readOnly={emojiMode === "inline"}
+ tabIndex={emojiMode === "inline" ? -1 : undefined}
+ />
+
diff --git a/src/components/form/RichTextField.tsx b/src/components/form/RichTextField.tsx
index 18940ce3..b5477b88 100644
--- a/src/components/form/RichTextField.tsx
+++ b/src/components/form/RichTextField.tsx
@@ -1,4 +1,5 @@
import { useId } from "react";
+import { t } from "@lingui/core/macro";
import {
Controller,
type FieldValues,
@@ -14,6 +15,7 @@ interface RichTextFieldProps {
label?: string;
placeholder?: string;
rows?: number;
+ /** Counted in the editor and enforced on submit, with a message saying so. */
maxLength?: number;
autoResize?: boolean;
disabled?: boolean;
@@ -42,8 +44,18 @@ export function RichTextField({
onSubmit,
}: RichTextFieldProps) {
const id = useId();
+ const hintId = useId();
const { control, formState: { errors } } = useFormContext();
const error = errors[name];
+ // A bare numeric rule fails without a message, which left a submit that did
+ // nothing and said nothing.
+ const allRules = maxLength == null ? rules : {
+ ...rules,
+ maxLength: {
+ value: maxLength,
+ message: t`At most ${maxLength} characters.`,
+ },
+ };
return (
@@ -55,7 +67,7 @@ export function RichTextField({
(
({
autoResize={autoResize}
disabled={disabled}
onSubmit={onSubmit}
+ aria-invalid={error ? true : undefined}
+ aria-describedby={error?.message ? hintId : undefined}
/>
)}
/>
{error?.message && (
-
+
{String(error.message)}
)}
diff --git a/src/hooks/useEmojiTrigger.ts b/src/hooks/useEmojiTrigger.ts
index 1fd14349..c4236448 100644
--- a/src/hooks/useEmojiTrigger.ts
+++ b/src/hooks/useEmojiTrigger.ts
@@ -1,81 +1,97 @@
-import { type RefObject, useCallback, useRef, useState } from "react";
+import { useCallback, useRef, useState } from "react";
+import { insideCode } from "../utils/editorText.ts";
-// Trigger: ':' not preceded by a word character, followed by a letter then
-// optional word chars / '+' / '-'. Requiring a letter first prevents ASCII
-// emoticons like ':-' or ':(' from opening the picker.
-const TRIGGER_RE = /(? void,
- textareaRef: RefObject,
+ /** Replaces `[start, end)` of the textarea's value, with a space after
+ * when `word` is set; see TextEditor. */
+ replace: (start: number, end: number, text: string, word: boolean) => void,
) {
- const [state, setState] = useState({
- open: false,
- query: "",
- });
- // Saved positions at the moment the trigger was detected
- const triggerStartRef = useRef(0);
- const triggerCursorRef = useRef(0);
+ const [state, setStateRaw] = useState(CLOSED);
+ // Mirrored synchronously, so a change and a caret move in the same event
+ // each see what the other just decided.
+ const stateRef = useRef(state);
+ const setState = useCallback((next: EmojiState) => {
+ stateRef.current = next;
+ setStateRaw(next);
+ }, []);
/**
- * Call this from the textarea's onChange (alongside the mention handler).
- * It receives the new value and cursor position already read from the event.
+ * Call on every change (with `open`) and caret move (without): follows an
+ * inline `:query` as it's typed, and closes once the caret leaves it.
+ * Leaves a browse-mode picker alone.
*/
- const detectEmojiTrigger = useCallback(
- (newValue: string, cursor: number) => {
- const textBefore = newValue.slice(0, cursor);
- const match = TRIGGER_RE.exec(textBefore);
+ const syncEmojiTrigger = useCallback(
+ (value: string, cursor: number, open: boolean) => {
+ const s = stateRef.current;
+ if (s.mode === "browse") return;
+ if (!open && s.mode === "closed") return;
+ const before = value.slice(0, cursor);
+ const match = insideCode(before) ? null : TRIGGER_RE.exec(before);
if (match) {
- triggerStartRef.current = match.index;
- triggerCursorRef.current = cursor;
- setState({ open: true, query: match[1] });
- } else {
- setState((s) => s.open ? { open: false, query: "" } : s);
+ setState({
+ mode: "inline",
+ query: match[1],
+ start: match.index,
+ end: cursor,
+ });
+ } else if (s.mode !== "closed") {
+ setState(CLOSED);
}
},
- [],
+ [setState],
);
- /**
- * Called when frimousse fires onEmojiSelect.
- * Replaces the ':query' text with the selected emoji.
- */
- const handleEmojiSelect = useCallback(
- (native: string) => {
- const start = triggerStartRef.current;
- const cursor = triggerCursorRef.current;
- const newValue = value.slice(0, start) + native + " " +
- value.slice(cursor);
- onChange(newValue);
- setState({ open: false, query: "" });
- const newPos = start + native.length + 1; // +1 for trailing space
- requestAnimationFrame(() => {
- const el = textareaRef.current;
- if (el) {
- el.focus();
- el.setSelectionRange(newPos, newPos);
- }
- });
- },
- [value, onChange, textareaRef],
- );
+ const openEmojiBrowser = useCallback((start: number, end: number) => {
+ setState({ mode: "browse", query: "", start, end });
+ }, [setState]);
- const closeEmoji = useCallback(() => {
- setState({ open: false, query: "" });
- requestAnimationFrame(() => textareaRef.current?.focus());
- }, [textareaRef]);
+ const setEmojiQuery = useCallback((query: string) => {
+ const s = stateRef.current;
+ if (s.mode !== "closed") setState({ ...s, query });
+ }, [setState]);
+
+ const selectEmoji = useCallback((native: string) => {
+ const { mode, start, end } = stateRef.current;
+ if (mode === "closed") return;
+ setState(CLOSED);
+ // An inline pick finishes a word, so it takes a space after it the way a
+ // mention does; a browse pick goes in exactly where the caret was.
+ replace(start, end, native, mode === "inline");
+ }, [replace, setState]);
+
+ const closeEmoji = useCallback(() => setState(CLOSED), [setState]);
return {
- emojiOpen: state.open,
+ emojiMode: state.mode,
emojiQuery: state.query,
- detectEmojiTrigger,
- handleEmojiSelect,
+ syncEmojiTrigger,
+ openEmojiBrowser,
+ setEmojiQuery,
+ selectEmoji,
closeEmoji,
};
}
diff --git a/src/hooks/useMentionAutocomplete.ts b/src/hooks/useMentionAutocomplete.ts
index 18b2341d..5f0e2176 100644
--- a/src/hooks/useMentionAutocomplete.ts
+++ b/src/hooks/useMentionAutocomplete.ts
@@ -1,11 +1,6 @@
-import {
- type RefObject,
- useCallback,
- useEffect,
- useRef,
- useState,
-} from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import { API_URL } from "../config/api.ts";
+import { insideCode } from "../utils/editorText.ts";
export interface UserResult {
id: string;
@@ -16,7 +11,10 @@ export interface UserResult {
interface MentionState {
open: boolean;
query: string;
+ /** Where the `@` sits, and the caret after the query: the range a pick
+ * replaces. */
start: number;
+ end: number;
results: UserResult[];
selectedIndex: number;
}
@@ -25,146 +23,170 @@ const CLOSED: MentionState = {
open: false,
query: "",
start: 0,
+ end: 0,
results: [],
selectedIndex: 0,
};
+// The server's mention rule (api/services/notification-service.ts): an `@` not
+// preceded by `[`, `(` or a word character, so what's offered is exactly what
+// would notify.
+const TRIGGER_RE = /(? void,
- textareaRef: RefObject,
+ /** Replaces `[start, end)` of the textarea's value, with a space after
+ * when `word` is set; see TextEditor. */
+ replace: (start: number, end: number, text: string, word: boolean) => void,
) {
- const [state, setState] = useState(CLOSED);
+ const [state, setStateRaw] = useState(CLOSED);
+ // Mirrored synchronously so the key handler and a fetch landing late both
+ // read what the last keystroke decided, not the last render.
+ const stateRef = useRef(state);
+ const setState = useCallback((next: MentionState) => {
+ stateRef.current = next;
+ setStateRaw(next);
+ }, []);
const debounceRef = useRef | null>(null);
- const cursorRef = useRef(0);
+ const abortRef = useRef(null);
- const handleMentionChange = useCallback(
- (e: React.ChangeEvent) => {
- const newValue = e.target.value;
- const pos = e.target.selectionStart ?? newValue.length;
- cursorRef.current = pos;
- onChange(newValue);
+ const cancelLookup = useCallback(() => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = null;
+ abortRef.current?.abort();
+ abortRef.current = null;
+ }, []);
- const mention = getMentionQuery(newValue, pos);
+ const close = useCallback(() => {
+ cancelLookup();
+ if (stateRef.current !== CLOSED) setState(CLOSED);
+ }, [cancelLookup, setState]);
+
+ /**
+ * Call on every change (with `lookup`) and caret move (without): tracks the
+ * `@query` under the caret, searching for it after a short pause, and closes
+ * once the caret leaves it.
+ */
+ const syncMention = useCallback(
+ (value: string, pos: number, lookup: boolean) => {
+ const mention = getMentionQuery(value, pos);
+ const s = stateRef.current;
if (!mention) {
- setState(CLOSED);
- if (debounceRef.current) clearTimeout(debounceRef.current);
+ close();
return;
}
+ if (!lookup) {
+ // A caret move within the same @query keeps the list; one that lands
+ // on some other @word doesn't open anything.
+ if (mention.start !== s.start || mention.query !== s.query) close();
+ return;
+ }
+ if (mention.query === s.query && mention.start === s.start) return;
- setState((s) => ({
+ setState({
...s,
- open: false,
query: mention.query,
start: mention.start,
+ end: pos,
selectedIndex: 0,
- }));
+ });
- if (debounceRef.current) clearTimeout(debounceRef.current);
+ cancelLookup();
debounceRef.current = setTimeout(async () => {
+ const controller = new AbortController();
+ abortRef.current = controller;
try {
const res = await fetch(
`${API_URL}/api/users/search?q=${
encodeURIComponent(mention.query)
}`,
+ { signal: controller.signal },
);
const body = await res.json();
+ const current = stateRef.current;
+ // Only the lookup for what's typed now may open or close the list.
+ if (current.query !== mention.query) return;
if (body.success && body.data.length > 0) {
- setState((s) =>
- s.query === mention.query
- ? { ...s, open: true, results: body.data, selectedIndex: 0 }
- : s
- );
+ setState({
+ ...current,
+ open: true,
+ results: body.data,
+ selectedIndex: 0,
+ });
} else {
- setState(CLOSED);
+ setState({ ...current, open: false, results: [] });
}
} catch {
- setState(CLOSED);
+ // Aborted by a newer keystroke, or offline: nothing to offer.
}
}, 150);
},
- [onChange],
+ [cancelLookup, close, setState],
);
- const doSelect = useCallback(
- (username: string, start: number, cursorPos: number) => {
- const before = value.slice(0, start);
- const after = value.slice(cursorPos);
- onChange(`${before}@${username} ${after}`);
- setState(CLOSED);
- if (debounceRef.current) clearTimeout(debounceRef.current);
- setTimeout(() => {
- const el = textareaRef.current;
- if (el) {
- const newPos = start + username.length + 2; // @ + username + space
- el.focus();
- el.setSelectionRange(newPos, newPos);
- }
- }, 0);
+ const select = useCallback(
+ (username: string) => {
+ const { start, end } = stateRef.current;
+ close();
+ replace(start, end, `@${username}`, true);
},
- [value, onChange, textareaRef],
+ [close, replace],
);
+ /** Returns whether it handled the key. */
const handleMentionKeyDown = useCallback(
- (e: React.KeyboardEvent) => {
- if (!state.open || state.results.length === 0) return;
- if (e.key === "ArrowDown") {
+ (e: React.KeyboardEvent): boolean => {
+ const s = stateRef.current;
+ if (!s.open || s.results.length === 0) return false;
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
- setState((s) => ({
- ...s,
- selectedIndex: Math.min(s.selectedIndex + 1, s.results.length - 1),
- }));
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- setState((s) => ({
- ...s,
- selectedIndex: Math.max(s.selectedIndex - 1, 0),
- }));
- } else if (
- (e.key === "Enter" || e.key === "Tab") && !e.ctrlKey && !e.metaKey
+ const n = s.results.length;
+ const step = e.key === "ArrowDown" ? 1 : -1;
+ // Wraps, like any listbox.
+ setState({ ...s, selectedIndex: (s.selectedIndex + step + n) % n });
+ return true;
+ }
+ if (
+ (e.key === "Enter" || e.key === "Tab") &&
+ !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey
) {
e.preventDefault();
- const user = state.results[state.selectedIndex];
- if (user) doSelect(user.username, state.start, cursorRef.current);
- } else if (e.key === "Escape") {
- e.preventDefault();
- setState(CLOSED);
+ const user = s.results[s.selectedIndex];
+ if (user) select(user.username);
+ return true;
}
+ if (e.key === "Escape") {
+ // Just the list: whatever Escape means to the editor's owner (cancel
+ // an edit, close the chat) waits for a second press.
+ e.preventDefault();
+ e.stopPropagation();
+ close();
+ return true;
+ }
+ return false;
},
- [state, doSelect],
+ [close, select, setState],
);
- const handleMentionSelect = useCallback(
- (username: string) => {
- doSelect(username, state.start, cursorRef.current);
- },
- [doSelect, state.start],
- );
-
- useEffect(() => {
- return () => {
- if (debounceRef.current) clearTimeout(debounceRef.current);
- };
- }, []);
+ useEffect(() => cancelLookup, [cancelLookup]);
return {
- mentionOpen: state.open,
+ mentionOpen: state.open && state.results.length > 0,
mentionResults: state.results,
mentionSelectedIndex: state.selectedIndex,
- handleMentionChange,
+ syncMention,
+ closeMention: close,
handleMentionKeyDown,
- handleMentionSelect,
+ handleMentionSelect: select,
};
}
diff --git a/src/locales/en.js b/src/locales/en.js
index 3ba52add..af60c54e 100644
--- a/src/locales/en.js
+++ b/src/locales/en.js
@@ -1 +1 @@
-/*eslint-disable*/module.exports={messages:JSON.parse("{\"-K9EZb\":[\"Add email…\"],\"-OxI15\":[\"Followed playlists\"],\"-Ya-b9\":[\"Failed to save\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Search dumps, users, playlists…\"],\"1QfxQT\":[\"Dismiss\"],\"1YKhJ3\":[\"1 new message\"],\"1cbYY_\":[\"Upvoted (\",[\"0\"],[\"1\"],\")\"],\"1njn7W\":[\"Light\"],\"1utXA6\":[\"Dumps\"],\"26iNma\":[\"Post comment\"],\"2DoBvq\":[\"Feeds\"],\"2Hlmdt\":[\"Write a reply…\"],\"2_DHgA\":[\"URL slug\"],\"2nrsEn\":[\"deleted message\"],\"2ygf_L\":[\"← Back\"],\"3KKSM4\":[\"private\"],\"3QuNUr\":[\"Several people are typing…\"],\"3SEwY1\":[\"Delete category \\\"\",[\"0\"],\"\\\"? This cannot be undone.\"],\"3yfh3D\":[\"<0>\",[\"0\"],\"0> followed your playlist <1>\",[\"1\"],\"1>\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Dumped!\"],\"4GKuCs\":[\"Login failed\"],\"4HH9iB\":[\"Not following anyone yet.\"],\"4OjqAQ\":[\"Keep editing\"],\"4RtQ1k\":[\"Unfollow \",[\"targetUsername\"]],\"4c-qBx\":[\"Your password has been changed. You can now log in.\"],\"4yj9xV\":[\"Live updates are temporarily disconnected. Trying to reconnect…\"],\"5KHDCi\":[[\"0\"],\" and \",[\"1\"],\" are typing…\"],\"5TviPn\":[\"Cancel search\"],\"5cC8f2\":[\"Edited \",[\"0\"]],\"5oD9f_\":[\"Earlier\"],\"6Qly-0\":[\"a comment\"],\"6YE0P6\":[\"Editing \",[\"0\"]],\"6YtxFj\":[\"Name\"],\"6gRgw8\":[\"Retry\"],\"7PHCIN\":[\"Cancel removal\"],\"7PzzBU\":[\"User\"],\"7d1a0d\":[\"Public\"],\"7sNhEz\":[\"Username\"],\"8-8abB\":[\"Disconnected — reconnecting…\"],\"8F1i42\":[\"Page not found\"],\"8Ug9jB\":[\"Related\"],\"8ZsakT\":[\"Password\"],\"92o1-U\":[\"Failed to delete\"],\"9BruTc\":[\"Why?\"],\"9l4qcT\":[\"Drop a replacement here\"],\"9uI_rE\":[\"Undo\"],\"9xDZu_\":[\"Could not load.\"],\"A0y396\":[\"+ Invite someone\"],\"A1taO8\":[\"Search\"],\"AEfu-D\":[\"In collections\"],\"AHZflp\":[\"Chat\"],\"AQbgNR\":[\"Follow \",[\"targetUsername\"]],\"AS7WoE\":[\"Start fresh\"],\"ATGYL1\":[\"Email address\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"Ade-6d\":[\"Live updates unavailable.\"],\"AeXO77\":[\"Account\"],\"AnRu_j\":[\"Cancel reply\"],\"BlpJ2G\":[\"Picked up where you left off.\"],\"CI50ct\":[\"Upvoted\"],\"C_qQE4\":[\"Already dumped by \",[\"0\"],\" \",[\"1\"]],\"Cj24wt\":[\"Registering…\"],\"D8DRYV\":[\"Delete this message?\"],\"DPfwMq\":[\"Done\"],\"DdeHXH\":[\"Delete this dump? This cannot be undone.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\"0> upvoted <1>\",[\"1\"],\"1>\"],\"EBU6O6\":[\"Connection lost — this message may not have been sent\"],\"ECiS12\":[\"View dump →\"],\"ED8n0z\":[\"the chat\"],\"Evcm2Z\":[\"Close without posting? The attached file won't be kept — everything you typed will.\"],\"ExR0Fr\":[\"URL is required.\"],\"F5Js1v\":[\"Unfollow playlist\"],\"FdTlEG\":[\"Track \",[\"0\"]],\"FgAxTj\":[\"Log out\"],\"Fxf4jq\":[\"Description (optional)\"],\"GCWCCo\":[\"Close player\"],\"GDvlUT\":[\"Role\"],\"GLukaH\":[\"Moderator\"],\"GNSsCc\":[\"Failed to create playlist\"],\"GQ38dl\":[\"Chat (\",[\"unreadChatCount\"],\" unread)\"],\"GY6uSc\":[\"Next →\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" characters: letters, numbers, or underscores\"],\"GptGxg\":[\"Change password\"],\"H4MGkI\":[\"Expand player\"],\"H4o4sk\":[\"No one yet.\"],\"H8pzW-\":[\"Failed to update avatar\"],\"HTLDA4\":[\"Loading more…\"],\"I-x669\":[\"Invitees\"],\"IO9KL_\":[\"Next track\"],\"IZX7TO\":[\"Failed to generate invite\"],\"IagCbF\":[\"URL\"],\"ImOQa9\":[\"Reply\"],\"JJ-Bhk\":[\"Your email address\"],\"JRQitQ\":[\"Confirm new password\"],\"JXr41k\":[\"<0>\",[\"0\"],\"0> commented on <1>\",[\"1\"],\"1>\"],\"Jd58Fo\":[\"Hot\"],\"Jf0PuK\":[\"Go to login\"],\"JlFcis\":[\"Send\"],\"K7tIrx\":[\"Category\"],\"KDGWg5\":[\"Remove from playlist\"],\"KDw4GX\":[\"Try again\"],\"KKZxBU\":[\"Uploading \",[\"0\"],\"%\"],\"K_F6pa\":[\"Saving…\"],\"KtYF-t\":[\"Sounds\"],\"L-rMC9\":[\"Reset to default\"],\"LLyMkV\":[\"Followed (\",[\"0\"],[\"1\"],\")\"],\"MHrjPM\":[\"Title\"],\"MKEPCY\":[\"Follow\"],\"MTb2Ot\":[\"No messages yet. Say hello!\"],\"Mj6oCJ\":[[\"0\"],\" is typing…\"],\"N40H-G\":[\"All\"],\"N6j2JH\":[\"Edit \",[\"0\"]],\"NUrY9o\":[\"Categories\"],\"Oprv1v\":[\"Password (min. \",[\"0\"],\" characters)\"],\"Oz0N9s\":[\"new\"],\"PN3pBl\":[\"No reply from the server — this message may not have been sent\"],\"PUe8Yk\":[\"Collapse player\"],\"PiH3UR\":[\"Copied!\"],\"Pn2B7_\":[\"Current password\"],\"Pwqkdw\":[\"Loading…\"],\"Q6n4F4\":[\"Refresh metadata\"],\"QGaQRo\":[\"Failed to update role\"],\"QKsaQr\":[\"or <0>browse files0>\"],\"QLtPBd\":[\"No dumps in this playlist yet.\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Delete this playlist? This cannot be undone.\"],\"RTksSy\":[\"<0>\",[\"0\"],\"0> started following you\"],\"RaKjrM\":[\"Failed to save edit\"],\"RcUHRT\":[\"Followed\"],\"RfwZxd\":[\"Reset password\"],\"Rrp6-J\":[\"Log in to like\"],\"RvVJBH\":[\"Refreshing stream…\"],\"SBTElJ\":[\"Searching…\"],\"Sad2tK\":[\"Sending…\"],\"So7gCD\":[\"Paste a link…\"],\"StovX6\":[\"This page does not exist.\"],\"Sxm8rQ\":[\"Users\"],\"T9bjWt\":[\"<0>\",[\"0\"],\"0> was added to <1>\",[\"1\"],\"1>\"],\"TAtbho\":[\"Loading messages…\"],\"TM1ZbA\":[\"Playlists (\",[\"0\"],[\"1\"],\")\"],\"TN382O\":[\"Invalid link\"],\"Tv9vbB\":[\"Follow playlist\"],\"Tz0i8g\":[\"Settings\"],\"U3pytU\":[\"Admin\"],\"UNMVei\":[\"Forgot password?\"],\"UOZith\":[\"Failed to post\"],\"URAieT\":[\"Log in to vote\"],\"VCoEm-\":[\"Back to login\"],\"VKsaTi\":[\"Replying to\"],\"V_e7nf\":[\"Set new password\"],\"VnNJbN\":[\"From playlists\"],\"VskHIx\":[\"Delete category\"],\"VyTYmS\":[\"Change avatar\"],\"W9FRBT\":[\"Like\"],\"WE_r9K\":[\"Failed to create category\"],\"WfffrI\":[\"slug\"],\"WhimMi\":[\"Reset failed\"],\"WpXcBJ\":[\"Nothing here yet.\"],\"XHqhoS\":[\"Couldn't load the chat.\"],\"XJVYo8\":[\"Why & where\"],\"XJy2oN\":[\"Logging in…\"],\"Xan6QP\":[\"New dump\"],\"XgRtUf\":[\"Could not change password\"],\"Xi0Mn4\":[\"← Back to profile\"],\"XnL-Eu\":[\"No users match \\\"\",[\"q\"],\"\\\".\"],\"Xs2Lez\":[\"This reset link is missing or malformed.\"],\"YK1Dhc\":[\"a post\"],\"YcjhhX\":[\"User management\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\"0> liked your comment on <1>\",[\"1\"],\"1>\"],\"YpkCca\":[\"No followed playlists yet.\"],\"Z5HWHd\":[\"On\"],\"ZBdbv9\":[\"Edit title\"],\"ZCpU0u\":[\"No playlists match \\\"\",[\"q\"],\"\\\".\"],\"ZmD2o6\":[\"Create & Add\"],\"_3O5R_\":[\"Request failed\"],\"_DwR-n\":[\"Creating…\"],\"_R_sGB\":[\"Password changed successfully.\"],\"_aept4\":[\"Post reply\"],\"_karOe\":[\"You're offline — not sent\"],\"_nT6AE\":[\"New password\"],\"_t4W-i\":[\"From people\"],\"aAIQg2\":[\"Appearance\"],\"aDvLhk\":[\"Add a comment…\"],\"az8lvo\":[\"Off\"],\"b3Thhd\":[\"Upload failed\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"bQhwn-\":[\"Loading playlist…\"],\"bzSI52\":[\"Discard\"],\"cILfnJ\":[\"Remove file\"],\"cNu4my\":[[\"0\"],\" more online: \",[\"restNames\"]],\"cYP9Sb\":[\"+ Playlist\"],\"cbeBbZ\":[\"At least \",[\"0\"],\" characters\"],\"cnGeoo\":[\"Delete\"],\"dAs22m\":[\"Replace file\"],\"dEgA5A\":[\"Cancel\"],\"dMizp8\":[\"New playlist\"],\"dau1Pl\":[[\"unseenCount\"],\" new messages\"],\"dqwCfR\":[\"Couldn't load a preview for this link — check it, or post it as-is and refresh the preview later.\"],\"eFSqvc\":[\"Failed to post reply\"],\"eOfXq3\":[\"Title is required.\"],\"ePK91l\":[\"Edit\"],\"eaUTwS\":[\"Send reset link\"],\"ecUA8p\":[\"Today\"],\"ef9nPf\":[\"Loading dump…\"],\"eloqXQ\":[\"You can post it anyway.\"],\"en9o7K\":[\"Failed to post comment\"],\"etFQQS\":[\"What makes it worth it?\"],\"fC6mXb\":[\"Upvote\"],\"fI-mNw\":[\"Playlists\"],\"f_akpP\":[\"Max 50 MB\"],\"fgLNSM\":[\"Register\"],\"gANddk\":[\"Uploading…\"],\"gGx5tM\":[\"Editing\"],\"gIQQwD\":[\"Failed to load\"],\"gL9KKi\":[\"Who can see it\"],\"gLfZlz\":[\"Add to playlist\"],\"gjJ-sb\":[\"Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects.\"],\"goWEhR\":[\"Previous track\"],\"hBuUKa\":[\"Change password…\"],\"hD7w09\":[\"You've reached the end.\"],\"hJSliC\":[\"<0>\",[\"0\"],\"0> posted <1>\",[\"1\"],\"1>\"],\"hT8hgO\":[\"Reset to the suggested title\"],\"hYgDIe\":[\"Create\"],\"he3ygx\":[\"Copy\"],\"hpCc_Z\":[\"Close anyway\"],\"i7K_Te\":[\"Who am I?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Go home\"],\"ipYn7W\":[\"If that address is registered you'll receive a reset link shortly.\"],\"isRobC\":[\"New\"],\"jbernk\":[\"Loading profile…\"],\"joEmfT\":[\"Server unreachable\"],\"jrZTZl\":[\"No dumps yet. Be the first!\"],\"k112DD\":[\"Load older messages\"],\"k36iJ-\":[\"Failed to save category\"],\"k4b5_X\":[\"edited\"],\"kLttbL\":[\"Registration failed\"],\"lUDifl\":[\"Created (\",[\"0\"],[\"1\"],\")\"],\"lUanmi\":[\"You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}]],\"lcfvr_\":[\"Delete this comment?\"],\"lpIMne\":[\"Passwords do not match\"],\"mb_Wju\":[\"Drop a file here, or paste one\"],\"mt6O6E\":[\"This is a mirage.\"],\"nbm5sI\":[\"No dumps match \\\"\",[\"q\"],\"\\\".\"],\"nrjqON\":[\"Checking invite…\"],\"nwtY4N\":[\"Something went wrong\"],\"nx4kaN\":[\"Could not save\"],\"oTYvvN\":[[\"0\",\"plural\",{\"one\":[\"and \",\"#\",\" earlier dump\"],\"other\":[\"and \",\"#\",\" earlier dumps\"]}]],\"ogtYkT\":[\"Password updated\"],\"pCpd9p\":[\"<0>\",[\"0\"],\"0> mentioned you in <1>\",[\"where\"],\"1>\"],\"pSheLH\":[\"No invitees yet.\"],\"pvnfJD\":[\"Dark\"],\"qIMfNQ\":[\"Delete playlist\"],\"qbDAcy\":[\"Dump it\"],\"qgx_78\":[\"Follow some public playlists to see their dumps here.\"],\"qsqt0w\":[\"Invite link\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Dump\"],\"rCbqPX\":[\"This invite link is missing, expired, or already used.\"],\"rDMzDU\":[\"Type a message…\"],\"rg9pXu\":[\"Search failed\"],\"rtpJqV\":[\"Dumps (\",[\"0\"],[\"1\"],\")\"],\"sEXJPi\":[\"Link or file\"],\"sGeXL3\":[\"Thumbnail\"],\"sQia9P\":[\"Log in\"],\"sTiqbm\":[\"invited by\"],\"sdP5Aa\":[\"[deleted]\"],\"sgX7B-\":[\"Single use, valid for 7 days.\"],\"shHs8T\":[\"Enter a query to search.\"],\"smeBfS\":[\"Invalid invite\"],\"teaneZ\":[\"image\"],\"tfDRzk\":[\"Save\"],\"tvmuQ0\":[\"Color scheme\"],\"u1lDX2\":[\"Fetching preview…\"],\"uD0qXQ\":[\"Drop a file here\"],\"uMGUnV\":[\"No playlists yet.\"],\"ub1EEL\":[\"edited \",[\"0\"]],\"uomLas\":[\"Add category\"],\"vJBF1r\":[\"Posting…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" unread)\"],\"vQMkHu\":[\"Remove vote\"],\"vuosjb\":[\"User menu\"],\"wbXKOv\":[\"File too large (max 50 MB).\"],\"wckWOP\":[\"Manage\"],\"wixIgH\":[\"Already have an account? <0>Log in0>\"],\"x6tjuK\":[\"Default tab\"],\"xEWkgZ\":[\"← Back to all dumps\"],\"xPHtx0\":[\"Submit search\"],\"xVuNgt\":[\"+ New playlist\"],\"xc9O_u\":[\"Delete dump\"],\"y6sq5j\":[\"Following\"],\"y7oaHj\":[\"Playlist title\"],\"yA_6BX\":[\"View all →\"],\"yBBtRm\":[\"Follow some users to see their dumps here.\"],\"yQ2kGp\":[\"Load more\"],\"y_0uwd\":[\"Yesterday\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"z0ROB3\":[\"Remove like\"],\"z1uNN0\":[\"No emoji found.\"],\"zVuxvN\":[\"Refreshing…\"],\"zwBp5t\":[\"Private\"]}")};
\ No newline at end of file
+/*eslint-disable*/module.exports={messages:JSON.parse("{\"-K9EZb\":[\"Add email…\"],\"-OxI15\":[\"Followed playlists\"],\"-Ya-b9\":[\"Failed to save\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Search dumps, users, playlists…\"],\"1QfxQT\":[\"Dismiss\"],\"1YKhJ3\":[\"1 new message\"],\"1cbYY_\":[\"Upvoted (\",[\"0\"],[\"1\"],\")\"],\"1njn7W\":[\"Light\"],\"1utXA6\":[\"Dumps\"],\"26iNma\":[\"Post comment\"],\"2DoBvq\":[\"Feeds\"],\"2Hlmdt\":[\"Write a reply…\"],\"2_DHgA\":[\"URL slug\"],\"2nrsEn\":[\"deleted message\"],\"2ygf_L\":[\"← Back\"],\"3KKSM4\":[\"private\"],\"3QuNUr\":[\"Several people are typing…\"],\"3SEwY1\":[\"Delete category \\\"\",[\"0\"],\"\\\"? This cannot be undone.\"],\"3yfh3D\":[\"<0>\",[\"0\"],\"0> followed your playlist <1>\",[\"1\"],\"1>\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Dumped!\"],\"4GKuCs\":[\"Login failed\"],\"4HH9iB\":[\"Not following anyone yet.\"],\"4OjqAQ\":[\"Keep editing\"],\"4RtQ1k\":[\"Unfollow \",[\"targetUsername\"]],\"4c-qBx\":[\"Your password has been changed. You can now log in.\"],\"4yj9xV\":[\"Live updates are temporarily disconnected. Trying to reconnect…\"],\"5KHDCi\":[[\"0\"],\" and \",[\"1\"],\" are typing…\"],\"5TviPn\":[\"Cancel search\"],\"5cC8f2\":[\"Edited \",[\"0\"]],\"5oD9f_\":[\"Earlier\"],\"6Qly-0\":[\"a comment\"],\"6YE0P6\":[\"Editing \",[\"0\"]],\"6YtxFj\":[\"Name\"],\"6gRgw8\":[\"Retry\"],\"7PHCIN\":[\"Cancel removal\"],\"7PzzBU\":[\"User\"],\"7d1a0d\":[\"Public\"],\"7sNhEz\":[\"Username\"],\"8-8abB\":[\"Disconnected — reconnecting…\"],\"8F1i42\":[\"Page not found\"],\"8Ug9jB\":[\"Related\"],\"8ZsakT\":[\"Password\"],\"92o1-U\":[\"Failed to delete\"],\"9BruTc\":[\"Why?\"],\"9l4qcT\":[\"Drop a replacement here\"],\"9uI_rE\":[\"Undo\"],\"9xDZu_\":[\"Could not load.\"],\"A0y396\":[\"+ Invite someone\"],\"A1taO8\":[\"Search\"],\"AEfu-D\":[\"In collections\"],\"AHZflp\":[\"Chat\"],\"AQbgNR\":[\"Follow \",[\"targetUsername\"]],\"AS7WoE\":[\"Start fresh\"],\"ATGYL1\":[\"Email address\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"Ade-6d\":[\"Live updates unavailable.\"],\"AeXO77\":[\"Account\"],\"AnRu_j\":[\"Cancel reply\"],\"BlpJ2G\":[\"Picked up where you left off.\"],\"CI50ct\":[\"Upvoted\"],\"C_qQE4\":[\"Already dumped by \",[\"0\"],\" \",[\"1\"]],\"Cj24wt\":[\"Registering…\"],\"D8DRYV\":[\"Delete this message?\"],\"DPfwMq\":[\"Done\"],\"DdeHXH\":[\"Delete this dump? This cannot be undone.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\"0> upvoted <1>\",[\"1\"],\"1>\"],\"EBU6O6\":[\"Connection lost — this message may not have been sent\"],\"ECiS12\":[\"View dump →\"],\"ED8n0z\":[\"the chat\"],\"EarrCe\":[\"Search emoji\"],\"Evcm2Z\":[\"Close without posting? The attached file won't be kept — everything you typed will.\"],\"ExR0Fr\":[\"URL is required.\"],\"F5Js1v\":[\"Unfollow playlist\"],\"FdTlEG\":[\"Track \",[\"0\"]],\"FgAxTj\":[\"Log out\"],\"Fxf4jq\":[\"Description (optional)\"],\"GCWCCo\":[\"Close player\"],\"GDvlUT\":[\"Role\"],\"GLukaH\":[\"Moderator\"],\"GNSsCc\":[\"Failed to create playlist\"],\"GQ38dl\":[\"Chat (\",[\"unreadChatCount\"],\" unread)\"],\"GY6uSc\":[\"Next →\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" characters: letters, numbers, or underscores\"],\"GptGxg\":[\"Change password\"],\"H4MGkI\":[\"Expand player\"],\"H4o4sk\":[\"No one yet.\"],\"H8pzW-\":[\"Failed to update avatar\"],\"HTLDA4\":[\"Loading more…\"],\"H_PnT8\":[\"Insert emoji\"],\"I-x669\":[\"Invitees\"],\"IO9KL_\":[\"Next track\"],\"IZX7TO\":[\"Failed to generate invite\"],\"IagCbF\":[\"URL\"],\"ImOQa9\":[\"Reply\"],\"JJ-Bhk\":[\"Your email address\"],\"JRQitQ\":[\"Confirm new password\"],\"JXr41k\":[\"<0>\",[\"0\"],\"0> commented on <1>\",[\"1\"],\"1>\"],\"Jd58Fo\":[\"Hot\"],\"Jf0PuK\":[\"Go to login\"],\"JlFcis\":[\"Send\"],\"K7tIrx\":[\"Category\"],\"KDGWg5\":[\"Remove from playlist\"],\"KDw4GX\":[\"Try again\"],\"KKZxBU\":[\"Uploading \",[\"0\"],\"%\"],\"K_F6pa\":[\"Saving…\"],\"KtYF-t\":[\"Sounds\"],\"L-rMC9\":[\"Reset to default\"],\"L7_rsx\":[\"Uploading image…\"],\"LLyMkV\":[\"Followed (\",[\"0\"],[\"1\"],\")\"],\"MHrjPM\":[\"Title\"],\"MKEPCY\":[\"Follow\"],\"MTb2Ot\":[\"No messages yet. Say hello!\"],\"Mj6oCJ\":[[\"0\"],\" is typing…\"],\"N40H-G\":[\"All\"],\"N6j2JH\":[\"Edit \",[\"0\"]],\"NUrY9o\":[\"Categories\"],\"Oprv1v\":[\"Password (min. \",[\"0\"],\" characters)\"],\"Oz0N9s\":[\"new\"],\"PN3pBl\":[\"No reply from the server — this message may not have been sent\"],\"PUe8Yk\":[\"Collapse player\"],\"PiH3UR\":[\"Copied!\"],\"Pn2B7_\":[\"Current password\"],\"Pwqkdw\":[\"Loading…\"],\"Q4WUjR\":[\"Search emoji…\"],\"Q6n4F4\":[\"Refresh metadata\"],\"QGaQRo\":[\"Failed to update role\"],\"QKsaQr\":[\"or <0>browse files0>\"],\"QLtPBd\":[\"No dumps in this playlist yet.\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Delete this playlist? This cannot be undone.\"],\"RTksSy\":[\"<0>\",[\"0\"],\"0> started following you\"],\"RaKjrM\":[\"Failed to save edit\"],\"RcUHRT\":[\"Followed\"],\"RfwZxd\":[\"Reset password\"],\"Rrp6-J\":[\"Log in to like\"],\"RvVJBH\":[\"Refreshing stream…\"],\"S1xV5B\":[\"At most \",[\"maxLength\"],\" characters.\"],\"SBTElJ\":[\"Searching…\"],\"Sad2tK\":[\"Sending…\"],\"So7gCD\":[\"Paste a link…\"],\"StovX6\":[\"This page does not exist.\"],\"Sxm8rQ\":[\"Users\"],\"T9bjWt\":[\"<0>\",[\"0\"],\"0> was added to <1>\",[\"1\"],\"1>\"],\"TAtbho\":[\"Loading messages…\"],\"TM1ZbA\":[\"Playlists (\",[\"0\"],[\"1\"],\")\"],\"TN382O\":[\"Invalid link\"],\"Tv9vbB\":[\"Follow playlist\"],\"Tz0i8g\":[\"Settings\"],\"U3pytU\":[\"Admin\"],\"UNMVei\":[\"Forgot password?\"],\"UOZith\":[\"Failed to post\"],\"URAieT\":[\"Log in to vote\"],\"VCoEm-\":[\"Back to login\"],\"VKsaTi\":[\"Replying to\"],\"V_e7nf\":[\"Set new password\"],\"VnNJbN\":[\"From playlists\"],\"VskHIx\":[\"Delete category\"],\"VyTYmS\":[\"Change avatar\"],\"W9FRBT\":[\"Like\"],\"WAcnnR\":[\"The image couldn't be uploaded.\"],\"WE_r9K\":[\"Failed to create category\"],\"WfffrI\":[\"slug\"],\"WhimMi\":[\"Reset failed\"],\"WpXcBJ\":[\"Nothing here yet.\"],\"XHqhoS\":[\"Couldn't load the chat.\"],\"XJVYo8\":[\"Why & where\"],\"XJy2oN\":[\"Logging in…\"],\"Xan6QP\":[\"New dump\"],\"XgRtUf\":[\"Could not change password\"],\"Xi0Mn4\":[\"← Back to profile\"],\"XnL-Eu\":[\"No users match \\\"\",[\"q\"],\"\\\".\"],\"Xs2Lez\":[\"This reset link is missing or malformed.\"],\"YK1Dhc\":[\"a post\"],\"YZxXK8\":[\"Close emoji picker\"],\"YcjhhX\":[\"User management\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\"0> liked your comment on <1>\",[\"1\"],\"1>\"],\"YpkCca\":[\"No followed playlists yet.\"],\"Z5HWHd\":[\"On\"],\"ZBdbv9\":[\"Edit title\"],\"ZCpU0u\":[\"No playlists match \\\"\",[\"q\"],\"\\\".\"],\"ZWxjuD\":[\"Some images couldn't be uploaded.\"],\"ZmD2o6\":[\"Create & Add\"],\"_3O5R_\":[\"Request failed\"],\"_DwR-n\":[\"Creating…\"],\"_R_sGB\":[\"Password changed successfully.\"],\"_aept4\":[\"Post reply\"],\"_karOe\":[\"You're offline — not sent\"],\"_nT6AE\":[\"New password\"],\"_t4W-i\":[\"From people\"],\"aAIQg2\":[\"Appearance\"],\"aDvLhk\":[\"Add a comment…\"],\"az8lvo\":[\"Off\"],\"b3Thhd\":[\"Upload failed\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"bQhwn-\":[\"Loading playlist…\"],\"bzSI52\":[\"Discard\"],\"cILfnJ\":[\"Remove file\"],\"cNu4my\":[[\"0\"],\" more online: \",[\"restNames\"]],\"cYP9Sb\":[\"+ Playlist\"],\"cbeBbZ\":[\"At least \",[\"0\"],\" characters\"],\"cnGeoo\":[\"Delete\"],\"dAs22m\":[\"Replace file\"],\"dEgA5A\":[\"Cancel\"],\"dMizp8\":[\"New playlist\"],\"dau1Pl\":[[\"unseenCount\"],\" new messages\"],\"dqwCfR\":[\"Couldn't load a preview for this link — check it, or post it as-is and refresh the preview later.\"],\"dznB-Y\":[\"Only images can be dropped here.\"],\"eFSqvc\":[\"Failed to post reply\"],\"eIEHHV\":[\"Change skin tone\"],\"eOfXq3\":[\"Title is required.\"],\"ePK91l\":[\"Edit\"],\"eaUTwS\":[\"Send reset link\"],\"ecUA8p\":[\"Today\"],\"ef9nPf\":[\"Loading dump…\"],\"eloqXQ\":[\"You can post it anyway.\"],\"en9o7K\":[\"Failed to post comment\"],\"etFQQS\":[\"What makes it worth it?\"],\"fC6mXb\":[\"Upvote\"],\"fI-mNw\":[\"Playlists\"],\"f_akpP\":[\"Max 50 MB\"],\"fgLNSM\":[\"Register\"],\"gANddk\":[\"Uploading…\"],\"gGx5tM\":[\"Editing\"],\"gIQQwD\":[\"Failed to load\"],\"gL9KKi\":[\"Who can see it\"],\"gLfZlz\":[\"Add to playlist\"],\"gjJ-sb\":[\"Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects.\"],\"goWEhR\":[\"Previous track\"],\"hBuUKa\":[\"Change password…\"],\"hD7w09\":[\"You've reached the end.\"],\"hJSliC\":[\"<0>\",[\"0\"],\"0> posted <1>\",[\"1\"],\"1>\"],\"hT8hgO\":[\"Reset to the suggested title\"],\"hYgDIe\":[\"Create\"],\"he3ygx\":[\"Copy\"],\"hpCc_Z\":[\"Close anyway\"],\"i7K_Te\":[\"Who am I?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Go home\"],\"ipYn7W\":[\"If that address is registered you'll receive a reset link shortly.\"],\"isRobC\":[\"New\"],\"jbernk\":[\"Loading profile…\"],\"joEmfT\":[\"Server unreachable\"],\"jrZTZl\":[\"No dumps yet. Be the first!\"],\"k112DD\":[\"Load older messages\"],\"k36iJ-\":[\"Failed to save category\"],\"k4b5_X\":[\"edited\"],\"kLttbL\":[\"Registration failed\"],\"lUDifl\":[\"Created (\",[\"0\"],[\"1\"],\")\"],\"lUanmi\":[\"You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}]],\"lcfvr_\":[\"Delete this comment?\"],\"lpIMne\":[\"Passwords do not match\"],\"mb_Wju\":[\"Drop a file here, or paste one\"],\"mt6O6E\":[\"This is a mirage.\"],\"nbm5sI\":[\"No dumps match \\\"\",[\"q\"],\"\\\".\"],\"nrjqON\":[\"Checking invite…\"],\"nwtY4N\":[\"Something went wrong\"],\"nx4kaN\":[\"Could not save\"],\"oTYvvN\":[[\"0\",\"plural\",{\"one\":[\"and \",\"#\",\" earlier dump\"],\"other\":[\"and \",\"#\",\" earlier dumps\"]}]],\"ogtYkT\":[\"Password updated\"],\"pCpd9p\":[\"<0>\",[\"0\"],\"0> mentioned you in <1>\",[\"where\"],\"1>\"],\"pSheLH\":[\"No invitees yet.\"],\"pvnfJD\":[\"Dark\"],\"qIMfNQ\":[\"Delete playlist\"],\"qbDAcy\":[\"Dump it\"],\"qgx_78\":[\"Follow some public playlists to see their dumps here.\"],\"qsqt0w\":[\"Invite link\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Dump\"],\"rCbqPX\":[\"This invite link is missing, expired, or already used.\"],\"rDMzDU\":[\"Type a message…\"],\"rg9pXu\":[\"Search failed\"],\"rtpJqV\":[\"Dumps (\",[\"0\"],[\"1\"],\")\"],\"sEXJPi\":[\"Link or file\"],\"sGeXL3\":[\"Thumbnail\"],\"sQia9P\":[\"Log in\"],\"sTiqbm\":[\"invited by\"],\"sdP5Aa\":[\"[deleted]\"],\"sgX7B-\":[\"Single use, valid for 7 days.\"],\"shHs8T\":[\"Enter a query to search.\"],\"smeBfS\":[\"Invalid invite\"],\"teaneZ\":[\"image\"],\"tfDRzk\":[\"Save\"],\"tvmuQ0\":[\"Color scheme\"],\"u1lDX2\":[\"Fetching preview…\"],\"uD0qXQ\":[\"Drop a file here\"],\"uMGUnV\":[\"No playlists yet.\"],\"ub1EEL\":[\"edited \",[\"0\"]],\"uomLas\":[\"Add category\"],\"vJBF1r\":[\"Posting…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" unread)\"],\"vQMkHu\":[\"Remove vote\"],\"vuosjb\":[\"User menu\"],\"wbXKOv\":[\"File too large (max 50 MB).\"],\"wckWOP\":[\"Manage\"],\"wixIgH\":[\"Already have an account? <0>Log in0>\"],\"x6tjuK\":[\"Default tab\"],\"xEWkgZ\":[\"← Back to all dumps\"],\"xPHtx0\":[\"Submit search\"],\"xVuNgt\":[\"+ New playlist\"],\"xc9O_u\":[\"Delete dump\"],\"y6sq5j\":[\"Following\"],\"y7oaHj\":[\"Playlist title\"],\"yA_6BX\":[\"View all →\"],\"yBBtRm\":[\"Follow some users to see their dumps here.\"],\"yQ2kGp\":[\"Load more\"],\"y_0uwd\":[\"Yesterday\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"z0ROB3\":[\"Remove like\"],\"z1uNN0\":[\"No emoji found.\"],\"zVuxvN\":[\"Refreshing…\"],\"zwBp5t\":[\"Private\"]}")};
\ No newline at end of file
diff --git a/src/locales/en.po b/src/locales/en.po
index 863455dc..7875f312 100644
--- a/src/locales/en.po
+++ b/src/locales/en.po
@@ -13,7 +13,7 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
-#: src/components/CommentThread.tsx:257
+#: src/components/CommentThread.tsx:270
msgid "[deleted]"
msgstr "[deleted]"
@@ -35,17 +35,17 @@ msgstr "{0, plural, one {and # earlier dump} other {and # earlier dumps}}"
#. placeholder {0}: typingNames[0]
#. placeholder {1}: typingNames[1]
-#: src/components/ChatModal.tsx:1063
+#: src/components/ChatModal.tsx:1079
msgid "{0} and {1} are typing…"
msgstr "{0} and {1} are typing…"
#. placeholder {0}: typingNames[0]
-#: src/components/ChatModal.tsx:1061
+#: src/components/ChatModal.tsx:1077
msgid "{0} is typing…"
msgstr "{0} is typing…"
#. placeholder {0}: rest.length
-#: src/components/PresenceRow.tsx:44
+#: src/components/PresenceRow.tsx:66
msgid "{0} more online: {restNames}"
msgstr "{0} more online: {restNames}"
@@ -59,15 +59,15 @@ msgstr "{0}–{1} characters: letters, numbers, or underscores"
msgid "{label} ({count})"
msgstr "{label} ({count})"
-#: src/components/ChatModal.tsx:1192
+#: src/components/ChatModal.tsx:1208
msgid "{unseenCount} new messages"
msgstr "{unseenCount} new messages"
-#: src/components/CommentThread.tsx:462
+#: src/components/CommentThread.tsx:475
msgid "{visibleCount, plural, one {# comment} other {# comments}}"
msgstr "{visibleCount, plural, one {# comment} other {# comments}}"
-#: src/components/DumpCreateModal.tsx:943
+#: src/components/DumpCreateModal.tsx:942
#: src/pages/PlaylistDetail.tsx:572
#: src/pages/UserPublicProfile.tsx:790
msgid "← Back"
@@ -143,7 +143,7 @@ msgstr "<0>{0}0> upvoted <1>{1}1>"
msgid "<0>{0}0> was added to <1>{1}1>"
msgstr "<0>{0}0> was added to <1>{1}1>"
-#: src/components/ChatModal.tsx:1191
+#: src/components/ChatModal.tsx:1207
msgid "1 new message"
msgstr "1 new message"
@@ -159,7 +159,7 @@ msgstr "a post"
msgid "Account"
msgstr "Account"
-#: src/components/CommentThread.tsx:482
+#: src/components/CommentThread.tsx:495
msgid "Add a comment…"
msgstr "Add a comment…"
@@ -207,6 +207,10 @@ msgstr "Appearance"
msgid "At least {0} characters"
msgstr "At least {0} characters"
+#: src/components/form/RichTextField.tsx:56
+msgid "At most {maxLength} characters."
+msgstr "At most {maxLength} characters."
+
#: src/pages/UserPublicProfile.tsx:1252
msgid "Auto"
msgstr "Auto"
@@ -223,16 +227,16 @@ msgstr "Can't connect to the live updates server. Upvotes and notifications may
#: src/components/CategoryManager.tsx:210
#: src/components/CategoryManager.tsx:211
-#: src/components/ChatModal.tsx:224
-#: src/components/CommentThread.tsx:121
+#: src/components/ChatModal.tsx:235
+#: src/components/CommentThread.tsx:129
#: src/components/ConfirmModal.tsx:34
-#: src/components/DumpCreateModal.tsx:953
+#: src/components/DumpCreateModal.tsx:952
#: src/components/form/FormActions.tsx:32
#: src/pages/Dump.tsx:402
-#: src/pages/DumpEdit.tsx:484
-#: src/pages/PlaylistDetail.tsx:923
+#: src/pages/DumpEdit.tsx:483
+#: src/pages/PlaylistDetail.tsx:929
#: src/pages/UserPublicProfile.tsx:1764
-#: src/pages/UserPublicProfile.tsx:1837
+#: src/pages/UserPublicProfile.tsx:1838
msgid "Cancel"
msgstr "Cancel"
@@ -240,12 +244,12 @@ msgstr "Cancel"
msgid "Cancel removal"
msgstr "Cancel removal"
-#: src/components/ChatModal.tsx:1252
-#: src/components/ChatModal.tsx:1253
+#: src/components/ChatModal.tsx:1268
+#: src/components/ChatModal.tsx:1269
msgid "Cancel reply"
msgstr "Cancel reply"
-#: src/components/AppHeader.tsx:139
+#: src/components/AppHeader.tsx:127
msgid "Cancel search"
msgstr "Cancel search"
@@ -271,10 +275,14 @@ msgstr "Change password"
msgid "Change password…"
msgstr "Change password…"
+#: src/components/TextEditor.tsx:619
+msgid "Change skin tone"
+msgstr "Change skin tone"
+
#: src/components/ChatButton.tsx:17
#: src/components/ChatFab.tsx:49
#: src/components/ChatFab.tsx:50
-#: src/components/ChatModal.tsx:1152
+#: src/components/ChatModal.tsx:1168
msgid "Chat"
msgstr "Chat"
@@ -296,6 +304,10 @@ msgstr "Close"
msgid "Close anyway"
msgstr "Close anyway"
+#: src/components/TextEditor.tsx:628
+msgid "Close emoji picker"
+msgstr "Close emoji picker"
+
#: src/components/GlobalPlayer.tsx:242
msgid "Close player"
msgstr "Close player"
@@ -345,15 +357,15 @@ msgstr "Could not save"
msgid "Couldn't load a preview for this link — check it, or post it as-is and refresh the preview later."
msgstr "Couldn't load a preview for this link — check it, or post it as-is and refresh the preview later."
-#: src/components/ChatModal.tsx:1078
+#: src/components/ChatModal.tsx:1094
msgid "Couldn't load the chat."
msgstr "Couldn't load the chat."
-#: src/components/PlaylistCreateForm.tsx:91
+#: src/components/PlaylistCreateForm.tsx:90
msgid "Create"
msgstr "Create"
-#: src/components/PlaylistCreateForm.tsx:91
+#: src/components/PlaylistCreateForm.tsx:90
msgid "Create & Add"
msgstr "Create & Add"
@@ -363,7 +375,7 @@ msgstr "Create & Add"
msgid "Created ({0}{1})"
msgstr "Created ({0}{1})"
-#: src/components/PlaylistCreateForm.tsx:90
+#: src/components/PlaylistCreateForm.tsx:89
msgid "Creating…"
msgstr "Creating…"
@@ -381,13 +393,13 @@ msgstr "Default tab"
#: src/components/CategoryManager.tsx:221
#: src/components/CategoryManager.tsx:222
-#: src/components/ChatModal.tsx:336
-#: src/components/ChatModal.tsx:337
-#: src/components/ChatModal.tsx:431
-#: src/components/CommentThread.tsx:382
-#: src/components/CommentThread.tsx:388
+#: src/components/ChatModal.tsx:347
+#: src/components/ChatModal.tsx:348
+#: src/components/ChatModal.tsx:446
+#: src/components/CommentThread.tsx:395
+#: src/components/CommentThread.tsx:401
#: src/components/ConfirmModal.tsx:17
-#: src/pages/PlaylistDetail.tsx:919
+#: src/pages/PlaylistDetail.tsx:925
msgid "Delete"
msgstr "Delete"
@@ -401,7 +413,7 @@ msgid "Delete category \"{0}\"? This cannot be undone."
msgstr "Delete category \"{0}\"? This cannot be undone."
#: src/pages/DumpEdit.tsx:265
-#: src/pages/DumpEdit.tsx:480
+#: src/pages/DumpEdit.tsx:479
msgid "Delete dump"
msgstr "Delete dump"
@@ -411,7 +423,7 @@ msgstr "Delete dump"
msgid "Delete playlist"
msgstr "Delete playlist"
-#: src/components/CommentThread.tsx:387
+#: src/components/CommentThread.tsx:400
msgid "Delete this comment?"
msgstr "Delete this comment?"
@@ -419,7 +431,7 @@ msgstr "Delete this comment?"
msgid "Delete this dump? This cannot be undone."
msgstr "Delete this dump? This cannot be undone."
-#: src/components/ChatModal.tsx:430
+#: src/components/ChatModal.tsx:445
msgid "Delete this message?"
msgstr "Delete this message?"
@@ -428,25 +440,25 @@ msgstr "Delete this message?"
msgid "Delete this playlist? This cannot be undone."
msgstr "Delete this playlist? This cannot be undone."
-#: src/components/ChatModal.tsx:258
+#: src/components/ChatModal.tsx:269
msgid "deleted message"
msgstr "deleted message"
#: src/components/PlaylistCreateForm.tsx:82
-#: src/pages/PlaylistDetail.tsx:880
+#: src/pages/PlaylistDetail.tsx:886
msgid "Description (optional)"
msgstr "Description (optional)"
-#: src/components/ChatModal.tsx:516
+#: src/components/ChatModal.tsx:533
msgid "Discard"
msgstr "Discard"
-#: src/components/ChatModal.tsx:1156
+#: src/components/ChatModal.tsx:1172
msgid "Disconnected — reconnecting…"
msgstr "Disconnected — reconnecting…"
-#: src/components/ChatModal.tsx:1228
-#: src/components/ChatModal.tsx:1229
+#: src/components/ChatModal.tsx:1244
+#: src/components/ChatModal.tsx:1245
msgid "Dismiss"
msgstr "Dismiss"
@@ -466,11 +478,11 @@ msgstr "Drop a file here, or paste one"
msgid "Drop a replacement here"
msgstr "Drop a replacement here"
-#: src/components/AppHeader.tsx:179
+#: src/components/AppHeader.tsx:167
msgid "Dump"
msgstr "Dump"
-#: src/components/DumpCreateModal.tsx:966
+#: src/components/DumpCreateModal.tsx:965
msgid "Dump it"
msgstr "Dump it"
@@ -494,10 +506,10 @@ msgstr "Dumps ({0}{1})"
msgid "Earlier"
msgstr "Earlier"
-#: src/components/ChatModal.tsx:325
-#: src/components/ChatModal.tsx:326
-#: src/components/ChatModal.tsx:509
-#: src/components/CommentThread.tsx:373
+#: src/components/ChatModal.tsx:336
+#: src/components/ChatModal.tsx:337
+#: src/components/ChatModal.tsx:526
+#: src/components/CommentThread.tsx:386
#: src/pages/Dump.tsx:516
#: src/pages/PlaylistDetail.tsx:627
msgid "Edit"
@@ -512,15 +524,15 @@ msgstr "Edit {0}"
msgid "Edit title"
msgstr "Edit title"
-#: src/components/ChatModal.tsx:349
+#: src/components/ChatModal.tsx:360
msgid "edited"
msgstr "edited"
#. placeholder {0}: relativeTime(comment.updatedAt)
#. placeholder {0}: relativeTime(dump.updatedAt)
#. placeholder {0}: relativeTime(message.updatedAt)
-#: src/components/ChatModal.tsx:350
-#: src/components/CommentThread.tsx:322
+#: src/components/ChatModal.tsx:361
+#: src/components/CommentThread.tsx:335
#: src/pages/Dump.tsx:455
#: src/pages/PlaylistDetail.tsx:666
msgid "edited {0}"
@@ -529,8 +541,8 @@ msgstr "edited {0}"
#. placeholder {0}: comment.updatedAt.toLocaleString()
#. placeholder {0}: dump.updatedAt.toLocaleString()
#. placeholder {0}: message.updatedAt.toLocaleString()
-#: src/components/ChatModal.tsx:346
-#: src/components/CommentThread.tsx:320
+#: src/components/ChatModal.tsx:357
+#: src/components/CommentThread.tsx:333
#: src/pages/Dump.tsx:453
#: src/pages/PlaylistDetail.tsx:663
msgid "Edited {0}"
@@ -561,7 +573,7 @@ msgstr "Expand player"
msgid "Failed to create category"
msgstr "Failed to create category"
-#: src/components/PlaylistCreateForm.tsx:88
+#: src/components/PlaylistCreateForm.tsx:87
msgid "Failed to create playlist"
msgstr "Failed to create playlist"
@@ -590,17 +602,17 @@ msgstr "Failed to load"
msgid "Failed to post"
msgstr "Failed to post"
-#: src/components/CommentThread.tsx:481
+#: src/components/CommentThread.tsx:494
msgid "Failed to post comment"
msgstr "Failed to post comment"
-#: src/components/CommentThread.tsx:403
+#: src/components/CommentThread.tsx:416
msgid "Failed to post reply"
msgstr "Failed to post reply"
-#: src/pages/PlaylistDetail.tsx:911
+#: src/pages/PlaylistDetail.tsx:917
#: src/pages/UserPublicProfile.tsx:1767
-#: src/pages/UserPublicProfile.tsx:1839
+#: src/pages/UserPublicProfile.tsx:1840
msgid "Failed to save"
msgstr "Failed to save"
@@ -608,7 +620,7 @@ msgstr "Failed to save"
msgid "Failed to save category"
msgstr "Failed to save category"
-#: src/components/CommentThread.tsx:335
+#: src/components/CommentThread.tsx:348
msgid "Failed to save edit"
msgstr "Failed to save edit"
@@ -704,7 +716,7 @@ msgstr "Hot"
msgid "If that address is registered you'll receive a reset link shortly."
msgstr "If that address is registered you'll receive a reset link shortly."
-#: src/components/ChatModal.tsx:81
+#: src/components/ChatModal.tsx:95
msgid "image"
msgstr "image"
@@ -712,6 +724,11 @@ msgstr "image"
msgid "In collections"
msgstr "In collections"
+#: src/components/TextEditor.tsx:504
+#: src/components/TextEditor.tsx:506
+msgid "Insert emoji"
+msgstr "Insert emoji"
+
#: src/pages/UserRegister.tsx:96
msgid "Invalid invite"
msgstr "Invalid invite"
@@ -763,7 +780,7 @@ msgstr "Link or file"
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
-#: src/components/AppHeader.tsx:212
+#: src/components/AppHeader.tsx:200
msgid "Live updates unavailable."
msgstr "Live updates unavailable."
@@ -772,7 +789,7 @@ msgstr "Live updates unavailable."
msgid "Load more"
msgstr "Load more"
-#: src/components/ChatModal.tsx:1177
+#: src/components/ChatModal.tsx:1193
msgid "Load older messages"
msgstr "Load older messages"
@@ -781,7 +798,7 @@ msgstr "Load older messages"
msgid "Loading dump…"
msgstr "Loading dump…"
-#: src/components/ChatModal.tsx:1070
+#: src/components/ChatModal.tsx:1086
msgid "Loading messages…"
msgstr "Loading messages…"
@@ -806,9 +823,9 @@ msgid "Loading profile…"
msgstr "Loading profile…"
#: src/components/CategoryManager.tsx:52
-#: src/components/ChatModal.tsx:1176
+#: src/components/ChatModal.tsx:1192
#: src/components/PlaylistMembershipPanel.tsx:28
-#: src/components/TextEditor.tsx:328
+#: src/components/TextEditor.tsx:640
#: src/components/UserListPopover.tsx:192
#: src/components/UserListPopover.tsx:230
#: src/pages/index/FollowedFeed.tsx:78
@@ -826,7 +843,7 @@ msgstr "Loading profile…"
msgid "Loading…"
msgstr "Loading…"
-#: src/components/AppHeader.tsx:187
+#: src/components/AppHeader.tsx:175
#: src/pages/UserLogin.tsx:35
#: src/pages/UserLogin.tsx:61
#: src/pages/UserLogin.tsx:80
@@ -875,7 +892,7 @@ msgstr "Name"
msgid "new"
msgstr "new"
-#: src/components/ChatModal.tsx:1105
+#: src/components/ChatModal.tsx:1121
#: src/components/FeedTabBar.tsx:31
#: src/pages/UserPublicProfile.tsx:1294
msgid "New"
@@ -899,7 +916,7 @@ msgstr "New password"
msgid "New playlist"
msgstr "New playlist"
-#: src/components/DumpCreateModal.tsx:965
+#: src/components/DumpCreateModal.tsx:964
msgid "Next →"
msgstr "Next →"
@@ -921,7 +938,7 @@ msgstr "No dumps match \"{q}\"."
msgid "No dumps yet. Be the first!"
msgstr "No dumps yet. Be the first!"
-#: src/components/TextEditor.tsx:331
+#: src/components/TextEditor.tsx:643
msgid "No emoji found."
msgstr "No emoji found."
@@ -934,7 +951,7 @@ msgstr "No followed playlists yet."
msgid "No invitees yet."
msgstr "No invitees yet."
-#: src/components/ChatModal.tsx:1089
+#: src/components/ChatModal.tsx:1105
msgid "No messages yet. Say hello!"
msgstr "No messages yet. Say hello!"
@@ -991,6 +1008,10 @@ msgstr "Off"
msgid "On"
msgstr "On"
+#: src/components/TextEditor.tsx:338
+msgid "Only images can be dropped here."
+msgstr "Only images can be dropped here."
+
#: src/components/FileDropZone.tsx:134
msgid "or <0>browse files0>"
msgstr "or <0>browse files0>"
@@ -1030,11 +1051,11 @@ msgstr "Paste a link…"
msgid "Picked up where you left off."
msgstr "Picked up where you left off."
-#: src/pages/PlaylistDetail.tsx:866
+#: src/pages/PlaylistDetail.tsx:870
msgid "Playlist title"
msgstr "Playlist title"
-#: src/components/AppHeader.tsx:160
+#: src/components/AppHeader.tsx:148
#: src/components/DumpCreateModal.tsx:68
#: src/components/UserMenu.tsx:62
#: src/pages/Search.tsx:177
@@ -1049,17 +1070,17 @@ msgstr "Playlists"
msgid "Playlists ({0}{1})"
msgstr "Playlists ({0}{1})"
-#: src/components/CommentThread.tsx:479
+#: src/components/CommentThread.tsx:492
msgid "Post comment"
msgstr "Post comment"
-#: src/components/CommentThread.tsx:401
+#: src/components/CommentThread.tsx:414
msgid "Post reply"
msgstr "Post reply"
-#: src/components/CommentThread.tsx:402
-#: src/components/CommentThread.tsx:480
-#: src/components/DumpCreateModal.tsx:959
+#: src/components/CommentThread.tsx:415
+#: src/components/CommentThread.tsx:493
+#: src/components/DumpCreateModal.tsx:958
msgid "Posting…"
msgstr "Posting…"
@@ -1077,7 +1098,7 @@ msgid "private"
msgstr "private"
#: src/components/form/SegmentedField.tsx:81
-#: src/pages/PlaylistDetail.tsx:905
+#: src/pages/PlaylistDetail.tsx:911
msgid "Private"
msgstr "Private"
@@ -1087,7 +1108,7 @@ msgid "public"
msgstr "public"
#: src/components/form/SegmentedField.tsx:80
-#: src/pages/PlaylistDetail.tsx:898
+#: src/pages/PlaylistDetail.tsx:904
msgid "Public"
msgstr "Public"
@@ -1141,13 +1162,13 @@ msgstr "Remove vote"
msgid "Replace file"
msgstr "Replace file"
-#: src/components/ChatModal.tsx:315
-#: src/components/ChatModal.tsx:316
-#: src/components/CommentThread.tsx:361
+#: src/components/ChatModal.tsx:326
+#: src/components/ChatModal.tsx:327
+#: src/components/CommentThread.tsx:374
msgid "Reply"
msgstr "Reply"
-#: src/components/ChatModal.tsx:1240
+#: src/components/ChatModal.tsx:1256
msgid "Replying to"
msgstr "Replying to"
@@ -1173,7 +1194,7 @@ msgstr "Reset to default"
msgid "Reset to the suggested title"
msgstr "Reset to the suggested title"
-#: src/components/ChatModal.tsx:502
+#: src/components/ChatModal.tsx:519
#: src/pages/Dump.tsx:283
#: src/pages/DumpEdit.tsx:180
msgid "Retry"
@@ -1183,27 +1204,27 @@ msgstr "Retry"
msgid "Role"
msgstr "Role"
-#: src/components/ChatModal.tsx:221
-#: src/components/CommentThread.tsx:333
+#: src/components/ChatModal.tsx:232
+#: src/components/CommentThread.tsx:346
#: src/pages/Dump.tsx:394
-#: src/pages/DumpEdit.tsx:487
-#: src/pages/PlaylistDetail.tsx:930
+#: src/pages/DumpEdit.tsx:486
+#: src/pages/PlaylistDetail.tsx:936
#: src/pages/UserPublicProfile.tsx:1756
-#: src/pages/UserPublicProfile.tsx:1829
+#: src/pages/UserPublicProfile.tsx:1830
msgid "Save"
msgstr "Save"
#: src/components/ChangePasswordModal.tsx:103
-#: src/components/CommentThread.tsx:334
+#: src/components/CommentThread.tsx:347
#: src/pages/Dump.tsx:393
-#: src/pages/PlaylistDetail.tsx:926
+#: src/pages/PlaylistDetail.tsx:932
#: src/pages/ResetPassword.tsx:126
#: src/pages/UserPublicProfile.tsx:1753
-#: src/pages/UserPublicProfile.tsx:1826
+#: src/pages/UserPublicProfile.tsx:1827
msgid "Saving…"
msgstr "Saving…"
-#: src/components/AppHeader.tsx:139
+#: src/components/AppHeader.tsx:127
#: src/components/SearchBar.tsx:73
#: src/pages/Search.tsx:50
msgid "Search"
@@ -1213,6 +1234,14 @@ msgstr "Search"
msgid "Search dumps, users, playlists…"
msgstr "Search dumps, users, playlists…"
+#: src/components/TextEditor.tsx:612
+msgid "Search emoji"
+msgstr "Search emoji"
+
+#: src/components/TextEditor.tsx:611
+msgid "Search emoji…"
+msgstr "Search emoji…"
+
#: src/pages/Search.tsx:218
msgid "Search failed"
msgstr "Search failed"
@@ -1221,7 +1250,7 @@ msgstr "Search failed"
msgid "Searching…"
msgstr "Searching…"
-#: src/components/ChatModal.tsx:1280
+#: src/components/ChatModal.tsx:1296
msgid "Send"
msgstr "Send"
@@ -1229,12 +1258,12 @@ msgstr "Send"
msgid "Send reset link"
msgstr "Send reset link"
-#: src/components/ChatModal.tsx:520
+#: src/components/ChatModal.tsx:537
#: src/pages/UserLogin.tsx:142
msgid "Sending…"
msgstr "Sending…"
-#: src/components/AppHeader.tsx:176
+#: src/components/AppHeader.tsx:164
msgid "Server unreachable"
msgstr "Server unreachable"
@@ -1247,7 +1276,7 @@ msgstr "Set new password"
msgid "Settings"
msgstr "Settings"
-#: src/components/ChatModal.tsx:1064
+#: src/components/ChatModal.tsx:1080
msgid "Several people are typing…"
msgstr "Several people are typing…"
@@ -1259,6 +1288,10 @@ msgstr "Single use, valid for 7 days."
msgid "slug"
msgstr "slug"
+#: src/components/TextEditor.tsx:297
+msgid "Some images couldn't be uploaded."
+msgstr "Some images couldn't be uploaded."
+
#: src/components/form/FormError.tsx:15
#: src/components/PageError.tsx:13
#: src/contexts/WSProvider.tsx:663
@@ -1285,6 +1318,10 @@ msgstr "Submit search"
msgid "the chat"
msgstr "the chat"
+#: src/components/TextEditor.tsx:298
+msgid "The image couldn't be uploaded."
+msgstr "The image couldn't be uploaded."
+
#: src/pages/UserRegister.tsx:97
msgid "This invite link is missing, expired, or already used."
msgstr "This invite link is missing, expired, or already used."
@@ -1315,7 +1352,7 @@ msgstr "Title"
msgid "Title is required."
msgstr "Title is required."
-#: src/components/ChatModal.tsx:133
+#: src/components/ChatModal.tsx:147
#: src/pages/Notifications.tsx:398
msgid "Today"
msgstr "Today"
@@ -1325,11 +1362,11 @@ msgstr "Today"
msgid "Track {0}"
msgstr "Track {0}"
-#: src/components/ChatModal.tsx:1081
+#: src/components/ChatModal.tsx:1097
msgid "Try again"
msgstr "Try again"
-#: src/components/ChatModal.tsx:1265
+#: src/components/ChatModal.tsx:1281
msgid "Type a message…"
msgstr "Type a message…"
@@ -1354,7 +1391,11 @@ msgstr "Upload failed"
msgid "Uploading {0}%"
msgstr "Uploading {0}%"
-#: src/components/DumpCreateModal.tsx:962
+#: src/components/TextEditor.tsx:526
+msgid "Uploading image…"
+msgstr "Uploading image…"
+
+#: src/components/DumpCreateModal.tsx:961
msgid "Uploading…"
msgstr "Uploading…"
@@ -1423,11 +1464,11 @@ msgid "What makes it worth it?"
msgstr "What makes it worth it?"
#: src/pages/UserPublicProfile.tsx:925
-#: src/pages/UserPublicProfile.tsx:1818
+#: src/pages/UserPublicProfile.tsx:1819
msgid "Who am I?"
msgstr "Who am I?"
-#: src/components/DumpCreateModal.tsx:928
+#: src/components/DumpCreateModal.tsx:927
msgid "Who can see it"
msgstr "Who can see it"
@@ -1440,11 +1481,11 @@ msgstr "Why & where"
msgid "Why?"
msgstr "Why?"
-#: src/components/CommentThread.tsx:405
+#: src/components/CommentThread.tsx:418
msgid "Write a reply…"
msgstr "Write a reply…"
-#: src/components/ChatModal.tsx:136
+#: src/components/ChatModal.tsx:150
#: src/pages/Notifications.tsx:400
msgid "Yesterday"
msgstr "Yesterday"
diff --git a/src/locales/fr.js b/src/locales/fr.js
index 07ef3660..30f4907b 100644
--- a/src/locales/fr.js
+++ b/src/locales/fr.js
@@ -1 +1 @@
-/*eslint-disable*/module.exports={messages:JSON.parse("{\"-K9EZb\":[\"Ajouter un e-mail…\"],\"-OxI15\":[\"Collections suivies\"],\"-Ya-b9\":[\"Enregistrement échoué\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Rechercher des recos, utilisateurs, collections…\"],\"1QfxQT\":[\"Fermer\"],\"1YKhJ3\":[\"1 nouveau message\"],\"1cbYY_\":[\"Votés (\",[\"0\"],[\"1\"],\")\"],\"1njn7W\":[\"Clair\"],\"1utXA6\":[\"Recos\"],\"26iNma\":[\"Publier le commentaire\"],\"2DoBvq\":[\"Flux\"],\"2Hlmdt\":[\"Écrire une réponse…\"],\"2_DHgA\":[\"Identifiant d’URL\"],\"2nrsEn\":[\"message supprimé\"],\"2ygf_L\":[\"← Retour\"],\"3KKSM4\":[\"privé\"],\"3QuNUr\":[\"Plusieurs personnes écrivent…\"],\"3SEwY1\":[\"Supprimer la catégorie \\\"\",[\"0\"],\"\\\" ? Cette action est irréversible.\"],\"3yfh3D\":[\"<0>\",[\"0\"],\"0> a suivi votre collection <1>\",[\"1\"],\"1>\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Recommandé !\"],\"4GKuCs\":[\"Connexion échouée\"],\"4HH9iB\":[\"Aucun abonnement pour le moment.\"],\"4OjqAQ\":[\"Continuer l'édition\"],\"4RtQ1k\":[\"Ne plus suivre \",[\"targetUsername\"]],\"4c-qBx\":[\"Votre mot de passe a été modifié. Vous pouvez maintenant vous connecter.\"],\"4yj9xV\":[\"Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…\"],\"5KHDCi\":[[\"0\"],\" et \",[\"1\"],\" sont en train d'écrire…\"],\"5TviPn\":[\"Annuler la recherche\"],\"5cC8f2\":[\"Modifié le \",[\"0\"]],\"5oD9f_\":[\"Plus tôt\"],\"6Qly-0\":[\"un commentaire\"],\"6YE0P6\":[\"Édition de \",[\"0\"]],\"6YtxFj\":[\"Nom\"],\"6gRgw8\":[\"Réessayer\"],\"7PHCIN\":[\"Annuler la suppression\"],\"7PzzBU\":[\"Utilisateur\"],\"7d1a0d\":[\"Public\"],\"7sNhEz\":[\"Nom d'utilisateur\"],\"8-8abB\":[\"Déconnecté — reconnexion…\"],\"8F1i42\":[\"Page introuvable\"],\"8Ug9jB\":[\"Connexe\"],\"8ZsakT\":[\"Mot de passe\"],\"92o1-U\":[\"Suppression échouée\"],\"9BruTc\":[\"Pourquoi ?\"],\"9l4qcT\":[\"Déposez un fichier de remplacement ici\"],\"9uI_rE\":[\"Annuler\"],\"9xDZu_\":[\"Impossible de charger.\"],\"A0y396\":[\"+ Inviter quelqu'un\"],\"A1taO8\":[\"Rechercher\"],\"AEfu-D\":[\"Dans les collections\"],\"AHZflp\":[\"Tribune\"],\"AQbgNR\":[\"Suivre \",[\"targetUsername\"]],\"AS7WoE\":[\"Repartir de zéro\"],\"ATGYL1\":[\"Adresse e-mail\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"Ade-6d\":[\"Mises à jour en direct indisponibles.\"],\"AeXO77\":[\"Compte\"],\"AnRu_j\":[\"Annuler la réponse\"],\"BlpJ2G\":[\"Reprise de votre brouillon.\"],\"CI50ct\":[\"Voté\"],\"C_qQE4\":[\"Déjà recommandé par \",[\"0\"],\" \",[\"1\"]],\"Cj24wt\":[\"Inscription…\"],\"D8DRYV\":[\"Supprimer ce message ?\"],\"DPfwMq\":[\"Terminé\"],\"DdeHXH\":[\"Supprimer cette reco ? Cette action est irréversible.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\"0> a voté pour <1>\",[\"1\"],\"1>\"],\"EBU6O6\":[\"Connexion perdue — ce message n'a peut-être pas été envoyé\"],\"ECiS12\":[\"Voir la reco →\"],\"ED8n0z\":[\"la tribune\"],\"Evcm2Z\":[\"Fermer sans publier ? Le fichier joint sera perdu — ce que vous avez écrit sera conservé.\"],\"ExR0Fr\":[\"L'URL est obligatoire.\"],\"F5Js1v\":[\"Ne plus suivre la collection\"],\"FdTlEG\":[\"Piste \",[\"0\"]],\"FgAxTj\":[\"Se déconnecter\"],\"Fxf4jq\":[\"Description (facultatif)\"],\"GCWCCo\":[\"Fermer le lecteur\"],\"GDvlUT\":[\"Rôle\"],\"GLukaH\":[\"Modérateur\"],\"GNSsCc\":[\"Impossible de créer la collection\"],\"GQ38dl\":[[\"unreadChatCount\"],\" messages non lus\"],\"GY6uSc\":[\"Suivant →\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" caractères : lettres, chiffres ou tirets bas\"],\"GptGxg\":[\"Changer le mot de passe\"],\"H4MGkI\":[\"Agrandir le lecteur\"],\"H4o4sk\":[\"Personne pour le moment.\"],\"H8pzW-\":[\"Impossible de mettre à jour l'avatar\"],\"HTLDA4\":[\"Chargement…\"],\"I-x669\":[\"Invités\"],\"IO9KL_\":[\"Piste suivante\"],\"IZX7TO\":[\"Impossible de générer une invitation\"],\"IagCbF\":[\"URL\"],\"ImOQa9\":[\"Répondre\"],\"JJ-Bhk\":[\"Votre adresse e-mail\"],\"JRQitQ\":[\"Confirmer le nouveau mot de passe\"],\"JXr41k\":[\"<0>\",[\"0\"],\"0> a commenté sur <1>\",[\"1\"],\"1>\"],\"Jd58Fo\":[\"Tendances\"],\"Jf0PuK\":[\"Aller à la connexion\"],\"JlFcis\":[\"Envoyer\"],\"K7tIrx\":[\"Catégorie\"],\"KDGWg5\":[\"Retirer de la collection\"],\"KDw4GX\":[\"Réessayer\"],\"KKZxBU\":[\"Envoi \",[\"0\"],\" %\"],\"K_F6pa\":[\"Enregistrement…\"],\"KtYF-t\":[\"Sons\"],\"L-rMC9\":[\"Réinitialiser par défaut\"],\"LLyMkV\":[\"Suivies (\",[\"0\"],[\"1\"],\")\"],\"MHrjPM\":[\"Titre\"],\"MKEPCY\":[\"Suivre\"],\"MTb2Ot\":[\"Aucun message pour l'instant. Dites bonjour !\"],\"Mj6oCJ\":[[\"0\"],\" est en train d'écrire…\"],\"N40H-G\":[\"Tout\"],\"N6j2JH\":[\"Modifier \",[\"0\"]],\"NUrY9o\":[\"Catégories\"],\"Oprv1v\":[\"Mot de passe (min. \",[\"0\"],\" caractères)\"],\"Oz0N9s\":[\"nouveau\"],\"PN3pBl\":[\"Pas de réponse du serveur — ce message n'a peut-être pas été envoyé\"],\"PUe8Yk\":[\"Réduire le lecteur\"],\"PiH3UR\":[\"Copié !\"],\"Pn2B7_\":[\"Mot de passe actuel\"],\"Pwqkdw\":[\"Chargement…\"],\"Q6n4F4\":[\"Actualiser les métadonnées\"],\"QGaQRo\":[\"Erreur lors de la mise à jour du rôle\"],\"QKsaQr\":[\"ou <0>parcourir les fichiers0>\"],\"QLtPBd\":[\"Aucune reco dans cette collection pour l'instant.\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Supprimer cette collection ? Cette action est irréversible.\"],\"RTksSy\":[\"<0>\",[\"0\"],\"0> a commencé à vous suivre\"],\"RaKjrM\":[\"Impossible d'enregistrer la modification\"],\"RcUHRT\":[\"Suivi\"],\"RfwZxd\":[\"Réinitialiser le mot de passe\"],\"Rrp6-J\":[\"Se connecter pour aimer\"],\"RvVJBH\":[\"Actualisation du flux…\"],\"SBTElJ\":[\"Recherche…\"],\"Sad2tK\":[\"Envoi…\"],\"So7gCD\":[\"Collez un lien…\"],\"StovX6\":[\"Rien à voir, circulez.\"],\"Sxm8rQ\":[\"Utilisateurs\"],\"T9bjWt\":[\"<0>\",[\"0\"],\"0> a été ajouté à <1>\",[\"1\"],\"1>\"],\"TAtbho\":[\"Chargement des messages…\"],\"TM1ZbA\":[\"Collections (\",[\"0\"],[\"1\"],\")\"],\"TN382O\":[\"Lien invalide\"],\"Tv9vbB\":[\"Suivre la collection\"],\"Tz0i8g\":[\"Paramètres\"],\"U3pytU\":[\"Administrateur\"],\"UNMVei\":[\"Mot de passe oublié ?\"],\"UOZith\":[\"Publication échouée\"],\"URAieT\":[\"Se connecter pour voter\"],\"VCoEm-\":[\"Retour à la connexion\"],\"VKsaTi\":[\"En réponse à\"],\"V_e7nf\":[\"Définir un nouveau mot de passe\"],\"VnNJbN\":[\"De collections\"],\"VskHIx\":[\"Supprimer la catégorie\"],\"VyTYmS\":[\"Changer l'avatar\"],\"W9FRBT\":[\"Aimer\"],\"WE_r9K\":[\"Échec de la création de la catégorie\"],\"WfffrI\":[\"identifiant\"],\"WhimMi\":[\"Échec de la réinitialisation\"],\"WpXcBJ\":[\"Rien ici pour l'instant.\"],\"XHqhoS\":[\"Impossible de charger le chat.\"],\"XJVYo8\":[\"Pourquoi & où\"],\"XJy2oN\":[\"Connexion…\"],\"Xan6QP\":[\"Nouvelle reco\"],\"XgRtUf\":[\"Impossible de changer le mot de passe\"],\"Xi0Mn4\":[\"← Retour au profil\"],\"XnL-Eu\":[\"Aucun utilisateur ne correspond à « \",[\"q\"],\" ».\"],\"Xs2Lez\":[\"Ce lien de réinitialisation est absent ou malformé.\"],\"YK1Dhc\":[\"une publication\"],\"YcjhhX\":[\"Gestion utilisateur\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\"0> a aimé votre commentaire sur <1>\",[\"1\"],\"1>\"],\"YpkCca\":[\"Pas encore de collections suivies.\"],\"Z5HWHd\":[\"Activés\"],\"ZBdbv9\":[\"Modifier le titre\"],\"ZCpU0u\":[\"Aucune collection ne correspond à « \",[\"q\"],\" ».\"],\"ZmD2o6\":[\"Créer et ajouter\"],\"_3O5R_\":[\"Échec de la demande\"],\"_DwR-n\":[\"Création…\"],\"_R_sGB\":[\"Mot de passe modifié avec succès.\"],\"_aept4\":[\"Publier la réponse\"],\"_karOe\":[\"Hors ligne — message non envoyé\"],\"_nT6AE\":[\"Nouveau mot de passe\"],\"_t4W-i\":[\"De personnes\"],\"aAIQg2\":[\"Apparence\"],\"aDvLhk\":[\"Ajouter un commentaire…\"],\"az8lvo\":[\"Désactivés\"],\"b3Thhd\":[\"Envoi échoué\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"bQhwn-\":[\"Chargement de la collection…\"],\"bzSI52\":[\"Abandonner\"],\"cILfnJ\":[\"Supprimer le fichier\"],\"cNu4my\":[[\"0\"],\" de plus en ligne : \",[\"restNames\"]],\"cYP9Sb\":[\"+ Collection\"],\"cbeBbZ\":[\"Au moins \",[\"0\"],\" caractères\"],\"cnGeoo\":[\"Supprimer\"],\"dAs22m\":[\"Remplacer le fichier\"],\"dEgA5A\":[\"Annuler\"],\"dMizp8\":[\"Nouvelle collection\"],\"dau1Pl\":[[\"unseenCount\"],\" nouveaux messages\"],\"dqwCfR\":[\"Impossible de charger un aperçu pour ce lien — vérifiez-le, ou publiez-le tel quel et actualisez l'aperçu plus tard.\"],\"eFSqvc\":[\"Impossible de publier la réponse\"],\"eOfXq3\":[\"Un titre est requis.\"],\"ePK91l\":[\"Modifier\"],\"eaUTwS\":[\"Envoyer le lien de réinitialisation\"],\"ecUA8p\":[\"Aujourd'hui\"],\"ef9nPf\":[\"Chargement de la reco…\"],\"eloqXQ\":[\"Vous pouvez la publier quand même.\"],\"en9o7K\":[\"Impossible de publier le commentaire\"],\"etFQQS\":[\"Pourquoi on en voudrait ?\"],\"fC6mXb\":[\"Voter\"],\"fI-mNw\":[\"Collections\"],\"f_akpP\":[\"Max 50 Mo\"],\"fgLNSM\":[\"S'inscrire\"],\"gANddk\":[\"Envoi…\"],\"gGx5tM\":[\"Modification\"],\"gIQQwD\":[\"Chargement échoué\"],\"gL9KKi\":[\"Qui peut la voir\"],\"gLfZlz\":[\"Ajouter à la collection\"],\"gjJ-sb\":[\"Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion.\"],\"goWEhR\":[\"Piste précédente\"],\"hBuUKa\":[\"Changer le mot de passe…\"],\"hD7w09\":[\"Vous avez tout lu, tout vu, tout bu.\"],\"hJSliC\":[\"<0>\",[\"0\"],\"0> a publié <1>\",[\"1\"],\"1>\"],\"hT8hgO\":[\"Rétablir le titre suggéré\"],\"hYgDIe\":[\"Créer\"],\"he3ygx\":[\"Copier\"],\"hpCc_Z\":[\"Fermer quand même\"],\"i7K_Te\":[\"Qui suis-je ?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Accueil\"],\"ipYn7W\":[\"Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu.\"],\"isRobC\":[\"Nouveau\"],\"jbernk\":[\"Chargement du profil…\"],\"joEmfT\":[\"Serveur inaccessible\"],\"jrZTZl\":[\"Pas encore de recos. Soyez le premier !\"],\"k112DD\":[\"Charger les messages plus anciens\"],\"k36iJ-\":[\"Échec de l’enregistrement de la catégorie\"],\"k4b5_X\":[\"modifié\"],\"kLttbL\":[\"Inscription échouée\"],\"lUDifl\":[\"Créées (\",[\"0\"],[\"1\"],\")\"],\"lUanmi\":[\"Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}]],\"lcfvr_\":[\"Supprimer ce commentaire ?\"],\"lpIMne\":[\"Les mots de passe ne correspondent pas\"],\"mb_Wju\":[\"Déposez un fichier ici, ou collez-en un\"],\"mt6O6E\":[\"C'est un mirage.\"],\"nbm5sI\":[\"Aucune reco ne correspond à « \",[\"q\"],\" ».\"],\"nrjqON\":[\"Vérification de l'invitation…\"],\"nwtY4N\":[\"Une erreur est survenue\"],\"nx4kaN\":[\"Sauvegarde impossible\"],\"oTYvvN\":[[\"0\",\"plural\",{\"one\":[\"et \",\"#\",\" reco plus ancienne\"],\"other\":[\"et \",\"#\",\" recos plus anciennes\"]}]],\"ogtYkT\":[\"Mot de passe mis à jour\"],\"pCpd9p\":[\"<0>\",[\"0\"],\"0> vous a mentionné dans <1>\",[\"where\"],\"1>\"],\"pSheLH\":[\"Aucun invité pour le moment.\"],\"pvnfJD\":[\"Sombre\"],\"qIMfNQ\":[\"Supprimer la collection\"],\"qbDAcy\":[\"Recommander\"],\"qgx_78\":[\"Suivez des collections publiques pour voir leurs recos ici.\"],\"qsqt0w\":[\"Lien d'invitation\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Reco\"],\"rCbqPX\":[\"Ce lien d'invitation est manquant, expiré ou déjà utilisé.\"],\"rDMzDU\":[\"Écrivez un message…\"],\"rg9pXu\":[\"Recherche échouée\"],\"rtpJqV\":[\"Recos (\",[\"0\"],[\"1\"],\")\"],\"sEXJPi\":[\"Lien ou fichier\"],\"sGeXL3\":[\"Miniature\"],\"sQia9P\":[\"Se connecter\"],\"sTiqbm\":[\"invité par\"],\"sdP5Aa\":[\"[supprimé]\"],\"sgX7B-\":[\"Usage unique, valable 7 jours.\"],\"shHs8T\":[\"Saisissez une recherche.\"],\"smeBfS\":[\"Invitation invalide\"],\"teaneZ\":[\"image\"],\"tfDRzk\":[\"Enregistrer\"],\"tvmuQ0\":[\"Thème de couleur\"],\"u1lDX2\":[\"Récupération de l'aperçu…\"],\"uD0qXQ\":[\"Déposez un fichier ici\"],\"uMGUnV\":[\"Pas encore de collections.\"],\"ub1EEL\":[\"modifié \",[\"0\"]],\"uomLas\":[\"Ajouter une catégorie\"],\"vJBF1r\":[\"Publication…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" non lues)\"],\"vQMkHu\":[\"Retirer le vote\"],\"vuosjb\":[\"Menu utilisateur\"],\"wbXKOv\":[\"Fichier trop volumineux (max 50 Mo).\"],\"wckWOP\":[\"Administration\"],\"wixIgH\":[\"Vous avez déjà un compte ? <0>Se connecter0>\"],\"x6tjuK\":[\"Onglet par défaut\"],\"xEWkgZ\":[\"← Retour à toutes les recos\"],\"xPHtx0\":[\"Lancer la recherche\"],\"xVuNgt\":[\"+ Nouvelle collection\"],\"xc9O_u\":[\"Supprimer la reco\"],\"y6sq5j\":[\"Abonné\"],\"y7oaHj\":[\"Titre de la collection\"],\"yA_6BX\":[\"Tout voir →\"],\"yBBtRm\":[\"Suivez des utilisateurs pour voir leurs recos ici.\"],\"yQ2kGp\":[\"Charger plus\"],\"y_0uwd\":[\"Hier\"],\"yz7wBu\":[\"Fermer\"],\"yzF66j\":[\"Lien\"],\"z0ROB3\":[\"Retirer le j'aime\"],\"z1uNN0\":[\"Aucun emoji trouvé.\"],\"zVuxvN\":[\"Actualisation…\"],\"zwBp5t\":[\"Privé\"]}")};
\ No newline at end of file
+/*eslint-disable*/module.exports={messages:JSON.parse("{\"-K9EZb\":[\"Ajouter un e-mail…\"],\"-OxI15\":[\"Collections suivies\"],\"-Ya-b9\":[\"Enregistrement échoué\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Rechercher des recos, utilisateurs, collections…\"],\"1QfxQT\":[\"Fermer\"],\"1YKhJ3\":[\"1 nouveau message\"],\"1cbYY_\":[\"Votés (\",[\"0\"],[\"1\"],\")\"],\"1njn7W\":[\"Clair\"],\"1utXA6\":[\"Recos\"],\"26iNma\":[\"Publier le commentaire\"],\"2DoBvq\":[\"Flux\"],\"2Hlmdt\":[\"Écrire une réponse…\"],\"2_DHgA\":[\"Identifiant d’URL\"],\"2nrsEn\":[\"message supprimé\"],\"2ygf_L\":[\"← Retour\"],\"3KKSM4\":[\"privé\"],\"3QuNUr\":[\"Plusieurs personnes écrivent…\"],\"3SEwY1\":[\"Supprimer la catégorie \\\"\",[\"0\"],\"\\\" ? Cette action est irréversible.\"],\"3yfh3D\":[\"<0>\",[\"0\"],\"0> a suivi votre collection <1>\",[\"1\"],\"1>\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Recommandé !\"],\"4GKuCs\":[\"Connexion échouée\"],\"4HH9iB\":[\"Aucun abonnement pour le moment.\"],\"4OjqAQ\":[\"Continuer l'édition\"],\"4RtQ1k\":[\"Ne plus suivre \",[\"targetUsername\"]],\"4c-qBx\":[\"Votre mot de passe a été modifié. Vous pouvez maintenant vous connecter.\"],\"4yj9xV\":[\"Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…\"],\"5KHDCi\":[[\"0\"],\" et \",[\"1\"],\" sont en train d'écrire…\"],\"5TviPn\":[\"Annuler la recherche\"],\"5cC8f2\":[\"Modifié le \",[\"0\"]],\"5oD9f_\":[\"Plus tôt\"],\"6Qly-0\":[\"un commentaire\"],\"6YE0P6\":[\"Édition de \",[\"0\"]],\"6YtxFj\":[\"Nom\"],\"6gRgw8\":[\"Réessayer\"],\"7PHCIN\":[\"Annuler la suppression\"],\"7PzzBU\":[\"Utilisateur\"],\"7d1a0d\":[\"Public\"],\"7sNhEz\":[\"Nom d'utilisateur\"],\"8-8abB\":[\"Déconnecté — reconnexion…\"],\"8F1i42\":[\"Page introuvable\"],\"8Ug9jB\":[\"Connexe\"],\"8ZsakT\":[\"Mot de passe\"],\"92o1-U\":[\"Suppression échouée\"],\"9BruTc\":[\"Pourquoi ?\"],\"9l4qcT\":[\"Déposez un fichier de remplacement ici\"],\"9uI_rE\":[\"Annuler\"],\"9xDZu_\":[\"Impossible de charger.\"],\"A0y396\":[\"+ Inviter quelqu'un\"],\"A1taO8\":[\"Rechercher\"],\"AEfu-D\":[\"Dans les collections\"],\"AHZflp\":[\"Tribune\"],\"AQbgNR\":[\"Suivre \",[\"targetUsername\"]],\"AS7WoE\":[\"Repartir de zéro\"],\"ATGYL1\":[\"Adresse e-mail\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"Ade-6d\":[\"Mises à jour en direct indisponibles.\"],\"AeXO77\":[\"Compte\"],\"AnRu_j\":[\"Annuler la réponse\"],\"BlpJ2G\":[\"Reprise de votre brouillon.\"],\"CI50ct\":[\"Voté\"],\"C_qQE4\":[\"Déjà recommandé par \",[\"0\"],\" \",[\"1\"]],\"Cj24wt\":[\"Inscription…\"],\"D8DRYV\":[\"Supprimer ce message ?\"],\"DPfwMq\":[\"Terminé\"],\"DdeHXH\":[\"Supprimer cette reco ? Cette action est irréversible.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\"0> a voté pour <1>\",[\"1\"],\"1>\"],\"EBU6O6\":[\"Connexion perdue — ce message n'a peut-être pas été envoyé\"],\"ECiS12\":[\"Voir la reco →\"],\"ED8n0z\":[\"la tribune\"],\"EarrCe\":[\"Rechercher un emoji\"],\"Evcm2Z\":[\"Fermer sans publier ? Le fichier joint sera perdu — ce que vous avez écrit sera conservé.\"],\"ExR0Fr\":[\"L'URL est obligatoire.\"],\"F5Js1v\":[\"Ne plus suivre la collection\"],\"FdTlEG\":[\"Piste \",[\"0\"]],\"FgAxTj\":[\"Se déconnecter\"],\"Fxf4jq\":[\"Description (facultatif)\"],\"GCWCCo\":[\"Fermer le lecteur\"],\"GDvlUT\":[\"Rôle\"],\"GLukaH\":[\"Modérateur\"],\"GNSsCc\":[\"Impossible de créer la collection\"],\"GQ38dl\":[[\"unreadChatCount\"],\" messages non lus\"],\"GY6uSc\":[\"Suivant →\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" caractères : lettres, chiffres ou tirets bas\"],\"GptGxg\":[\"Changer le mot de passe\"],\"H4MGkI\":[\"Agrandir le lecteur\"],\"H4o4sk\":[\"Personne pour le moment.\"],\"H8pzW-\":[\"Impossible de mettre à jour l'avatar\"],\"HTLDA4\":[\"Chargement…\"],\"H_PnT8\":[\"Insérer un emoji\"],\"I-x669\":[\"Invités\"],\"IO9KL_\":[\"Piste suivante\"],\"IZX7TO\":[\"Impossible de générer une invitation\"],\"IagCbF\":[\"URL\"],\"ImOQa9\":[\"Répondre\"],\"JJ-Bhk\":[\"Votre adresse e-mail\"],\"JRQitQ\":[\"Confirmer le nouveau mot de passe\"],\"JXr41k\":[\"<0>\",[\"0\"],\"0> a commenté sur <1>\",[\"1\"],\"1>\"],\"Jd58Fo\":[\"Tendances\"],\"Jf0PuK\":[\"Aller à la connexion\"],\"JlFcis\":[\"Envoyer\"],\"K7tIrx\":[\"Catégorie\"],\"KDGWg5\":[\"Retirer de la collection\"],\"KDw4GX\":[\"Réessayer\"],\"KKZxBU\":[\"Envoi \",[\"0\"],\" %\"],\"K_F6pa\":[\"Enregistrement…\"],\"KtYF-t\":[\"Sons\"],\"L-rMC9\":[\"Réinitialiser par défaut\"],\"L7_rsx\":[\"Envoi de l'image…\"],\"LLyMkV\":[\"Suivies (\",[\"0\"],[\"1\"],\")\"],\"MHrjPM\":[\"Titre\"],\"MKEPCY\":[\"Suivre\"],\"MTb2Ot\":[\"Aucun message pour l'instant. Dites bonjour !\"],\"Mj6oCJ\":[[\"0\"],\" est en train d'écrire…\"],\"N40H-G\":[\"Tout\"],\"N6j2JH\":[\"Modifier \",[\"0\"]],\"NUrY9o\":[\"Catégories\"],\"Oprv1v\":[\"Mot de passe (min. \",[\"0\"],\" caractères)\"],\"Oz0N9s\":[\"nouveau\"],\"PN3pBl\":[\"Pas de réponse du serveur — ce message n'a peut-être pas été envoyé\"],\"PUe8Yk\":[\"Réduire le lecteur\"],\"PiH3UR\":[\"Copié !\"],\"Pn2B7_\":[\"Mot de passe actuel\"],\"Pwqkdw\":[\"Chargement…\"],\"Q4WUjR\":[\"Rechercher un emoji…\"],\"Q6n4F4\":[\"Actualiser les métadonnées\"],\"QGaQRo\":[\"Erreur lors de la mise à jour du rôle\"],\"QKsaQr\":[\"ou <0>parcourir les fichiers0>\"],\"QLtPBd\":[\"Aucune reco dans cette collection pour l'instant.\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Supprimer cette collection ? Cette action est irréversible.\"],\"RTksSy\":[\"<0>\",[\"0\"],\"0> a commencé à vous suivre\"],\"RaKjrM\":[\"Impossible d'enregistrer la modification\"],\"RcUHRT\":[\"Suivi\"],\"RfwZxd\":[\"Réinitialiser le mot de passe\"],\"Rrp6-J\":[\"Se connecter pour aimer\"],\"RvVJBH\":[\"Actualisation du flux…\"],\"S1xV5B\":[[\"maxLength\"],\" caractères au maximum.\"],\"SBTElJ\":[\"Recherche…\"],\"Sad2tK\":[\"Envoi…\"],\"So7gCD\":[\"Collez un lien…\"],\"StovX6\":[\"Rien à voir, circulez.\"],\"Sxm8rQ\":[\"Utilisateurs\"],\"T9bjWt\":[\"<0>\",[\"0\"],\"0> a été ajouté à <1>\",[\"1\"],\"1>\"],\"TAtbho\":[\"Chargement des messages…\"],\"TM1ZbA\":[\"Collections (\",[\"0\"],[\"1\"],\")\"],\"TN382O\":[\"Lien invalide\"],\"Tv9vbB\":[\"Suivre la collection\"],\"Tz0i8g\":[\"Paramètres\"],\"U3pytU\":[\"Administrateur\"],\"UNMVei\":[\"Mot de passe oublié ?\"],\"UOZith\":[\"Publication échouée\"],\"URAieT\":[\"Se connecter pour voter\"],\"VCoEm-\":[\"Retour à la connexion\"],\"VKsaTi\":[\"En réponse à\"],\"V_e7nf\":[\"Définir un nouveau mot de passe\"],\"VnNJbN\":[\"De collections\"],\"VskHIx\":[\"Supprimer la catégorie\"],\"VyTYmS\":[\"Changer l'avatar\"],\"W9FRBT\":[\"Aimer\"],\"WAcnnR\":[\"L'image n'a pas pu être envoyée.\"],\"WE_r9K\":[\"Échec de la création de la catégorie\"],\"WfffrI\":[\"identifiant\"],\"WhimMi\":[\"Échec de la réinitialisation\"],\"WpXcBJ\":[\"Rien ici pour l'instant.\"],\"XHqhoS\":[\"Impossible de charger le chat.\"],\"XJVYo8\":[\"Pourquoi & où\"],\"XJy2oN\":[\"Connexion…\"],\"Xan6QP\":[\"Nouvelle reco\"],\"XgRtUf\":[\"Impossible de changer le mot de passe\"],\"Xi0Mn4\":[\"← Retour au profil\"],\"XnL-Eu\":[\"Aucun utilisateur ne correspond à « \",[\"q\"],\" ».\"],\"Xs2Lez\":[\"Ce lien de réinitialisation est absent ou malformé.\"],\"YK1Dhc\":[\"une publication\"],\"YZxXK8\":[\"Fermer le sélecteur d'emoji\"],\"YcjhhX\":[\"Gestion utilisateur\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\"0> a aimé votre commentaire sur <1>\",[\"1\"],\"1>\"],\"YpkCca\":[\"Pas encore de collections suivies.\"],\"Z5HWHd\":[\"Activés\"],\"ZBdbv9\":[\"Modifier le titre\"],\"ZCpU0u\":[\"Aucune collection ne correspond à « \",[\"q\"],\" ».\"],\"ZWxjuD\":[\"Certaines images n'ont pas pu être envoyées.\"],\"ZmD2o6\":[\"Créer et ajouter\"],\"_3O5R_\":[\"Échec de la demande\"],\"_DwR-n\":[\"Création…\"],\"_R_sGB\":[\"Mot de passe modifié avec succès.\"],\"_aept4\":[\"Publier la réponse\"],\"_karOe\":[\"Hors ligne — message non envoyé\"],\"_nT6AE\":[\"Nouveau mot de passe\"],\"_t4W-i\":[\"De personnes\"],\"aAIQg2\":[\"Apparence\"],\"aDvLhk\":[\"Ajouter un commentaire…\"],\"az8lvo\":[\"Désactivés\"],\"b3Thhd\":[\"Envoi échoué\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"bQhwn-\":[\"Chargement de la collection…\"],\"bzSI52\":[\"Abandonner\"],\"cILfnJ\":[\"Supprimer le fichier\"],\"cNu4my\":[[\"0\"],\" de plus en ligne : \",[\"restNames\"]],\"cYP9Sb\":[\"+ Collection\"],\"cbeBbZ\":[\"Au moins \",[\"0\"],\" caractères\"],\"cnGeoo\":[\"Supprimer\"],\"dAs22m\":[\"Remplacer le fichier\"],\"dEgA5A\":[\"Annuler\"],\"dMizp8\":[\"Nouvelle collection\"],\"dau1Pl\":[[\"unseenCount\"],\" nouveaux messages\"],\"dqwCfR\":[\"Impossible de charger un aperçu pour ce lien — vérifiez-le, ou publiez-le tel quel et actualisez l'aperçu plus tard.\"],\"dznB-Y\":[\"Seules les images peuvent être déposées ici.\"],\"eFSqvc\":[\"Impossible de publier la réponse\"],\"eIEHHV\":[\"Changer la couleur de peau\"],\"eOfXq3\":[\"Un titre est requis.\"],\"ePK91l\":[\"Modifier\"],\"eaUTwS\":[\"Envoyer le lien de réinitialisation\"],\"ecUA8p\":[\"Aujourd'hui\"],\"ef9nPf\":[\"Chargement de la reco…\"],\"eloqXQ\":[\"Vous pouvez la publier quand même.\"],\"en9o7K\":[\"Impossible de publier le commentaire\"],\"etFQQS\":[\"Pourquoi on en voudrait ?\"],\"fC6mXb\":[\"Voter\"],\"fI-mNw\":[\"Collections\"],\"f_akpP\":[\"Max 50 Mo\"],\"fgLNSM\":[\"S'inscrire\"],\"gANddk\":[\"Envoi…\"],\"gGx5tM\":[\"Modification\"],\"gIQQwD\":[\"Chargement échoué\"],\"gL9KKi\":[\"Qui peut la voir\"],\"gLfZlz\":[\"Ajouter à la collection\"],\"gjJ-sb\":[\"Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion.\"],\"goWEhR\":[\"Piste précédente\"],\"hBuUKa\":[\"Changer le mot de passe…\"],\"hD7w09\":[\"Vous avez tout lu, tout vu, tout bu.\"],\"hJSliC\":[\"<0>\",[\"0\"],\"0> a publié <1>\",[\"1\"],\"1>\"],\"hT8hgO\":[\"Rétablir le titre suggéré\"],\"hYgDIe\":[\"Créer\"],\"he3ygx\":[\"Copier\"],\"hpCc_Z\":[\"Fermer quand même\"],\"i7K_Te\":[\"Qui suis-je ?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Accueil\"],\"ipYn7W\":[\"Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu.\"],\"isRobC\":[\"Nouveau\"],\"jbernk\":[\"Chargement du profil…\"],\"joEmfT\":[\"Serveur inaccessible\"],\"jrZTZl\":[\"Pas encore de recos. Soyez le premier !\"],\"k112DD\":[\"Charger les messages plus anciens\"],\"k36iJ-\":[\"Échec de l’enregistrement de la catégorie\"],\"k4b5_X\":[\"modifié\"],\"kLttbL\":[\"Inscription échouée\"],\"lUDifl\":[\"Créées (\",[\"0\"],[\"1\"],\")\"],\"lUanmi\":[\"Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}]],\"lcfvr_\":[\"Supprimer ce commentaire ?\"],\"lpIMne\":[\"Les mots de passe ne correspondent pas\"],\"mb_Wju\":[\"Déposez un fichier ici, ou collez-en un\"],\"mt6O6E\":[\"C'est un mirage.\"],\"nbm5sI\":[\"Aucune reco ne correspond à « \",[\"q\"],\" ».\"],\"nrjqON\":[\"Vérification de l'invitation…\"],\"nwtY4N\":[\"Une erreur est survenue\"],\"nx4kaN\":[\"Sauvegarde impossible\"],\"oTYvvN\":[[\"0\",\"plural\",{\"one\":[\"et \",\"#\",\" reco plus ancienne\"],\"other\":[\"et \",\"#\",\" recos plus anciennes\"]}]],\"ogtYkT\":[\"Mot de passe mis à jour\"],\"pCpd9p\":[\"<0>\",[\"0\"],\"0> vous a mentionné dans <1>\",[\"where\"],\"1>\"],\"pSheLH\":[\"Aucun invité pour le moment.\"],\"pvnfJD\":[\"Sombre\"],\"qIMfNQ\":[\"Supprimer la collection\"],\"qbDAcy\":[\"Recommander\"],\"qgx_78\":[\"Suivez des collections publiques pour voir leurs recos ici.\"],\"qsqt0w\":[\"Lien d'invitation\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Reco\"],\"rCbqPX\":[\"Ce lien d'invitation est manquant, expiré ou déjà utilisé.\"],\"rDMzDU\":[\"Écrivez un message…\"],\"rg9pXu\":[\"Recherche échouée\"],\"rtpJqV\":[\"Recos (\",[\"0\"],[\"1\"],\")\"],\"sEXJPi\":[\"Lien ou fichier\"],\"sGeXL3\":[\"Miniature\"],\"sQia9P\":[\"Se connecter\"],\"sTiqbm\":[\"invité par\"],\"sdP5Aa\":[\"[supprimé]\"],\"sgX7B-\":[\"Usage unique, valable 7 jours.\"],\"shHs8T\":[\"Saisissez une recherche.\"],\"smeBfS\":[\"Invitation invalide\"],\"teaneZ\":[\"image\"],\"tfDRzk\":[\"Enregistrer\"],\"tvmuQ0\":[\"Thème de couleur\"],\"u1lDX2\":[\"Récupération de l'aperçu…\"],\"uD0qXQ\":[\"Déposez un fichier ici\"],\"uMGUnV\":[\"Pas encore de collections.\"],\"ub1EEL\":[\"modifié \",[\"0\"]],\"uomLas\":[\"Ajouter une catégorie\"],\"vJBF1r\":[\"Publication…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" non lues)\"],\"vQMkHu\":[\"Retirer le vote\"],\"vuosjb\":[\"Menu utilisateur\"],\"wbXKOv\":[\"Fichier trop volumineux (max 50 Mo).\"],\"wckWOP\":[\"Administration\"],\"wixIgH\":[\"Vous avez déjà un compte ? <0>Se connecter0>\"],\"x6tjuK\":[\"Onglet par défaut\"],\"xEWkgZ\":[\"← Retour à toutes les recos\"],\"xPHtx0\":[\"Lancer la recherche\"],\"xVuNgt\":[\"+ Nouvelle collection\"],\"xc9O_u\":[\"Supprimer la reco\"],\"y6sq5j\":[\"Abonné\"],\"y7oaHj\":[\"Titre de la collection\"],\"yA_6BX\":[\"Tout voir →\"],\"yBBtRm\":[\"Suivez des utilisateurs pour voir leurs recos ici.\"],\"yQ2kGp\":[\"Charger plus\"],\"y_0uwd\":[\"Hier\"],\"yz7wBu\":[\"Fermer\"],\"yzF66j\":[\"Lien\"],\"z0ROB3\":[\"Retirer le j'aime\"],\"z1uNN0\":[\"Aucun emoji trouvé.\"],\"zVuxvN\":[\"Actualisation…\"],\"zwBp5t\":[\"Privé\"]}")};
\ No newline at end of file
diff --git a/src/locales/fr.po b/src/locales/fr.po
index 4944253b..1f22423d 100644
--- a/src/locales/fr.po
+++ b/src/locales/fr.po
@@ -13,7 +13,7 @@ msgstr ""
"Last-Translator: \n"
"Language-Team: \n"
-#: src/components/CommentThread.tsx:257
+#: src/components/CommentThread.tsx:270
msgid "[deleted]"
msgstr "[supprimé]"
@@ -35,17 +35,17 @@ msgstr "{0, plural, one {et # reco plus ancienne} other {et # recos plus ancienn
#. placeholder {0}: typingNames[0]
#. placeholder {1}: typingNames[1]
-#: src/components/ChatModal.tsx:1063
+#: src/components/ChatModal.tsx:1079
msgid "{0} and {1} are typing…"
msgstr "{0} et {1} sont en train d'écrire…"
#. placeholder {0}: typingNames[0]
-#: src/components/ChatModal.tsx:1061
+#: src/components/ChatModal.tsx:1077
msgid "{0} is typing…"
msgstr "{0} est en train d'écrire…"
#. placeholder {0}: rest.length
-#: src/components/PresenceRow.tsx:44
+#: src/components/PresenceRow.tsx:66
msgid "{0} more online: {restNames}"
msgstr "{0} de plus en ligne : {restNames}"
@@ -59,15 +59,15 @@ msgstr "{0}–{1} caractères : lettres, chiffres ou tirets bas"
msgid "{label} ({count})"
msgstr "{label} ({count})"
-#: src/components/ChatModal.tsx:1192
+#: src/components/ChatModal.tsx:1208
msgid "{unseenCount} new messages"
msgstr "{unseenCount} nouveaux messages"
-#: src/components/CommentThread.tsx:462
+#: src/components/CommentThread.tsx:475
msgid "{visibleCount, plural, one {# comment} other {# comments}}"
msgstr "{visibleCount, plural, one {# commentaire} other {# commentaires}}"
-#: src/components/DumpCreateModal.tsx:943
+#: src/components/DumpCreateModal.tsx:942
#: src/pages/PlaylistDetail.tsx:572
#: src/pages/UserPublicProfile.tsx:790
msgid "← Back"
@@ -143,7 +143,7 @@ msgstr "<0>{0}0> a voté pour <1>{1}1>"
msgid "<0>{0}0> was added to <1>{1}1>"
msgstr "<0>{0}0> a été ajouté à <1>{1}1>"
-#: src/components/ChatModal.tsx:1191
+#: src/components/ChatModal.tsx:1207
msgid "1 new message"
msgstr "1 nouveau message"
@@ -159,7 +159,7 @@ msgstr "une publication"
msgid "Account"
msgstr "Compte"
-#: src/components/CommentThread.tsx:482
+#: src/components/CommentThread.tsx:495
msgid "Add a comment…"
msgstr "Ajouter un commentaire…"
@@ -207,6 +207,10 @@ msgstr "Apparence"
msgid "At least {0} characters"
msgstr "Au moins {0} caractères"
+#: src/components/form/RichTextField.tsx:56
+msgid "At most {maxLength} characters."
+msgstr "{maxLength} caractères au maximum."
+
#: src/pages/UserPublicProfile.tsx:1252
msgid "Auto"
msgstr "Auto"
@@ -223,16 +227,16 @@ msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les vo
#: src/components/CategoryManager.tsx:210
#: src/components/CategoryManager.tsx:211
-#: src/components/ChatModal.tsx:224
-#: src/components/CommentThread.tsx:121
+#: src/components/ChatModal.tsx:235
+#: src/components/CommentThread.tsx:129
#: src/components/ConfirmModal.tsx:34
-#: src/components/DumpCreateModal.tsx:953
+#: src/components/DumpCreateModal.tsx:952
#: src/components/form/FormActions.tsx:32
#: src/pages/Dump.tsx:402
-#: src/pages/DumpEdit.tsx:484
-#: src/pages/PlaylistDetail.tsx:923
+#: src/pages/DumpEdit.tsx:483
+#: src/pages/PlaylistDetail.tsx:929
#: src/pages/UserPublicProfile.tsx:1764
-#: src/pages/UserPublicProfile.tsx:1837
+#: src/pages/UserPublicProfile.tsx:1838
msgid "Cancel"
msgstr "Annuler"
@@ -240,12 +244,12 @@ msgstr "Annuler"
msgid "Cancel removal"
msgstr "Annuler la suppression"
-#: src/components/ChatModal.tsx:1252
-#: src/components/ChatModal.tsx:1253
+#: src/components/ChatModal.tsx:1268
+#: src/components/ChatModal.tsx:1269
msgid "Cancel reply"
msgstr "Annuler la réponse"
-#: src/components/AppHeader.tsx:139
+#: src/components/AppHeader.tsx:127
msgid "Cancel search"
msgstr "Annuler la recherche"
@@ -271,10 +275,14 @@ msgstr "Changer le mot de passe"
msgid "Change password…"
msgstr "Changer le mot de passe…"
+#: src/components/TextEditor.tsx:619
+msgid "Change skin tone"
+msgstr "Changer la couleur de peau"
+
#: src/components/ChatButton.tsx:17
#: src/components/ChatFab.tsx:49
#: src/components/ChatFab.tsx:50
-#: src/components/ChatModal.tsx:1152
+#: src/components/ChatModal.tsx:1168
msgid "Chat"
msgstr "Tribune"
@@ -296,6 +304,10 @@ msgstr "Fermer"
msgid "Close anyway"
msgstr "Fermer quand même"
+#: src/components/TextEditor.tsx:628
+msgid "Close emoji picker"
+msgstr "Fermer le sélecteur d'emoji"
+
#: src/components/GlobalPlayer.tsx:242
msgid "Close player"
msgstr "Fermer le lecteur"
@@ -345,15 +357,15 @@ msgstr "Sauvegarde impossible"
msgid "Couldn't load a preview for this link — check it, or post it as-is and refresh the preview later."
msgstr "Impossible de charger un aperçu pour ce lien — vérifiez-le, ou publiez-le tel quel et actualisez l'aperçu plus tard."
-#: src/components/ChatModal.tsx:1078
+#: src/components/ChatModal.tsx:1094
msgid "Couldn't load the chat."
msgstr "Impossible de charger le chat."
-#: src/components/PlaylistCreateForm.tsx:91
+#: src/components/PlaylistCreateForm.tsx:90
msgid "Create"
msgstr "Créer"
-#: src/components/PlaylistCreateForm.tsx:91
+#: src/components/PlaylistCreateForm.tsx:90
msgid "Create & Add"
msgstr "Créer et ajouter"
@@ -363,7 +375,7 @@ msgstr "Créer et ajouter"
msgid "Created ({0}{1})"
msgstr "Créées ({0}{1})"
-#: src/components/PlaylistCreateForm.tsx:90
+#: src/components/PlaylistCreateForm.tsx:89
msgid "Creating…"
msgstr "Création…"
@@ -381,13 +393,13 @@ msgstr "Onglet par défaut"
#: src/components/CategoryManager.tsx:221
#: src/components/CategoryManager.tsx:222
-#: src/components/ChatModal.tsx:336
-#: src/components/ChatModal.tsx:337
-#: src/components/ChatModal.tsx:431
-#: src/components/CommentThread.tsx:382
-#: src/components/CommentThread.tsx:388
+#: src/components/ChatModal.tsx:347
+#: src/components/ChatModal.tsx:348
+#: src/components/ChatModal.tsx:446
+#: src/components/CommentThread.tsx:395
+#: src/components/CommentThread.tsx:401
#: src/components/ConfirmModal.tsx:17
-#: src/pages/PlaylistDetail.tsx:919
+#: src/pages/PlaylistDetail.tsx:925
msgid "Delete"
msgstr "Supprimer"
@@ -401,7 +413,7 @@ msgid "Delete category \"{0}\"? This cannot be undone."
msgstr "Supprimer la catégorie \"{0}\" ? Cette action est irréversible."
#: src/pages/DumpEdit.tsx:265
-#: src/pages/DumpEdit.tsx:480
+#: src/pages/DumpEdit.tsx:479
msgid "Delete dump"
msgstr "Supprimer la reco"
@@ -411,7 +423,7 @@ msgstr "Supprimer la reco"
msgid "Delete playlist"
msgstr "Supprimer la collection"
-#: src/components/CommentThread.tsx:387
+#: src/components/CommentThread.tsx:400
msgid "Delete this comment?"
msgstr "Supprimer ce commentaire ?"
@@ -419,7 +431,7 @@ msgstr "Supprimer ce commentaire ?"
msgid "Delete this dump? This cannot be undone."
msgstr "Supprimer cette reco ? Cette action est irréversible."
-#: src/components/ChatModal.tsx:430
+#: src/components/ChatModal.tsx:445
msgid "Delete this message?"
msgstr "Supprimer ce message ?"
@@ -428,25 +440,25 @@ msgstr "Supprimer ce message ?"
msgid "Delete this playlist? This cannot be undone."
msgstr "Supprimer cette collection ? Cette action est irréversible."
-#: src/components/ChatModal.tsx:258
+#: src/components/ChatModal.tsx:269
msgid "deleted message"
msgstr "message supprimé"
#: src/components/PlaylistCreateForm.tsx:82
-#: src/pages/PlaylistDetail.tsx:880
+#: src/pages/PlaylistDetail.tsx:886
msgid "Description (optional)"
msgstr "Description (facultatif)"
-#: src/components/ChatModal.tsx:516
+#: src/components/ChatModal.tsx:533
msgid "Discard"
msgstr "Abandonner"
-#: src/components/ChatModal.tsx:1156
+#: src/components/ChatModal.tsx:1172
msgid "Disconnected — reconnecting…"
msgstr "Déconnecté — reconnexion…"
-#: src/components/ChatModal.tsx:1228
-#: src/components/ChatModal.tsx:1229
+#: src/components/ChatModal.tsx:1244
+#: src/components/ChatModal.tsx:1245
msgid "Dismiss"
msgstr "Fermer"
@@ -466,11 +478,11 @@ msgstr "Déposez un fichier ici, ou collez-en un"
msgid "Drop a replacement here"
msgstr "Déposez un fichier de remplacement ici"
-#: src/components/AppHeader.tsx:179
+#: src/components/AppHeader.tsx:167
msgid "Dump"
msgstr "Reco"
-#: src/components/DumpCreateModal.tsx:966
+#: src/components/DumpCreateModal.tsx:965
msgid "Dump it"
msgstr "Recommander"
@@ -494,10 +506,10 @@ msgstr "Recos ({0}{1})"
msgid "Earlier"
msgstr "Plus tôt"
-#: src/components/ChatModal.tsx:325
-#: src/components/ChatModal.tsx:326
-#: src/components/ChatModal.tsx:509
-#: src/components/CommentThread.tsx:373
+#: src/components/ChatModal.tsx:336
+#: src/components/ChatModal.tsx:337
+#: src/components/ChatModal.tsx:526
+#: src/components/CommentThread.tsx:386
#: src/pages/Dump.tsx:516
#: src/pages/PlaylistDetail.tsx:627
msgid "Edit"
@@ -512,15 +524,15 @@ msgstr "Modifier {0}"
msgid "Edit title"
msgstr "Modifier le titre"
-#: src/components/ChatModal.tsx:349
+#: src/components/ChatModal.tsx:360
msgid "edited"
msgstr "modifié"
#. placeholder {0}: relativeTime(comment.updatedAt)
#. placeholder {0}: relativeTime(dump.updatedAt)
#. placeholder {0}: relativeTime(message.updatedAt)
-#: src/components/ChatModal.tsx:350
-#: src/components/CommentThread.tsx:322
+#: src/components/ChatModal.tsx:361
+#: src/components/CommentThread.tsx:335
#: src/pages/Dump.tsx:455
#: src/pages/PlaylistDetail.tsx:666
msgid "edited {0}"
@@ -529,8 +541,8 @@ msgstr "modifié {0}"
#. placeholder {0}: comment.updatedAt.toLocaleString()
#. placeholder {0}: dump.updatedAt.toLocaleString()
#. placeholder {0}: message.updatedAt.toLocaleString()
-#: src/components/ChatModal.tsx:346
-#: src/components/CommentThread.tsx:320
+#: src/components/ChatModal.tsx:357
+#: src/components/CommentThread.tsx:333
#: src/pages/Dump.tsx:453
#: src/pages/PlaylistDetail.tsx:663
msgid "Edited {0}"
@@ -561,7 +573,7 @@ msgstr "Agrandir le lecteur"
msgid "Failed to create category"
msgstr "Échec de la création de la catégorie"
-#: src/components/PlaylistCreateForm.tsx:88
+#: src/components/PlaylistCreateForm.tsx:87
msgid "Failed to create playlist"
msgstr "Impossible de créer la collection"
@@ -590,17 +602,17 @@ msgstr "Chargement échoué"
msgid "Failed to post"
msgstr "Publication échouée"
-#: src/components/CommentThread.tsx:481
+#: src/components/CommentThread.tsx:494
msgid "Failed to post comment"
msgstr "Impossible de publier le commentaire"
-#: src/components/CommentThread.tsx:403
+#: src/components/CommentThread.tsx:416
msgid "Failed to post reply"
msgstr "Impossible de publier la réponse"
-#: src/pages/PlaylistDetail.tsx:911
+#: src/pages/PlaylistDetail.tsx:917
#: src/pages/UserPublicProfile.tsx:1767
-#: src/pages/UserPublicProfile.tsx:1839
+#: src/pages/UserPublicProfile.tsx:1840
msgid "Failed to save"
msgstr "Enregistrement échoué"
@@ -608,7 +620,7 @@ msgstr "Enregistrement échoué"
msgid "Failed to save category"
msgstr "Échec de l’enregistrement de la catégorie"
-#: src/components/CommentThread.tsx:335
+#: src/components/CommentThread.tsx:348
msgid "Failed to save edit"
msgstr "Impossible d'enregistrer la modification"
@@ -704,7 +716,7 @@ msgstr "Tendances"
msgid "If that address is registered you'll receive a reset link shortly."
msgstr "Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu."
-#: src/components/ChatModal.tsx:81
+#: src/components/ChatModal.tsx:95
msgid "image"
msgstr "image"
@@ -712,6 +724,11 @@ msgstr "image"
msgid "In collections"
msgstr "Dans les collections"
+#: src/components/TextEditor.tsx:504
+#: src/components/TextEditor.tsx:506
+msgid "Insert emoji"
+msgstr "Insérer un emoji"
+
#: src/pages/UserRegister.tsx:96
msgid "Invalid invite"
msgstr "Invitation invalide"
@@ -763,7 +780,7 @@ msgstr "Lien ou fichier"
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"
-#: src/components/AppHeader.tsx:212
+#: src/components/AppHeader.tsx:200
msgid "Live updates unavailable."
msgstr "Mises à jour en direct indisponibles."
@@ -772,7 +789,7 @@ msgstr "Mises à jour en direct indisponibles."
msgid "Load more"
msgstr "Charger plus"
-#: src/components/ChatModal.tsx:1177
+#: src/components/ChatModal.tsx:1193
msgid "Load older messages"
msgstr "Charger les messages plus anciens"
@@ -781,7 +798,7 @@ msgstr "Charger les messages plus anciens"
msgid "Loading dump…"
msgstr "Chargement de la reco…"
-#: src/components/ChatModal.tsx:1070
+#: src/components/ChatModal.tsx:1086
msgid "Loading messages…"
msgstr "Chargement des messages…"
@@ -806,9 +823,9 @@ msgid "Loading profile…"
msgstr "Chargement du profil…"
#: src/components/CategoryManager.tsx:52
-#: src/components/ChatModal.tsx:1176
+#: src/components/ChatModal.tsx:1192
#: src/components/PlaylistMembershipPanel.tsx:28
-#: src/components/TextEditor.tsx:328
+#: src/components/TextEditor.tsx:640
#: src/components/UserListPopover.tsx:192
#: src/components/UserListPopover.tsx:230
#: src/pages/index/FollowedFeed.tsx:78
@@ -826,7 +843,7 @@ msgstr "Chargement du profil…"
msgid "Loading…"
msgstr "Chargement…"
-#: src/components/AppHeader.tsx:187
+#: src/components/AppHeader.tsx:175
#: src/pages/UserLogin.tsx:35
#: src/pages/UserLogin.tsx:61
#: src/pages/UserLogin.tsx:80
@@ -875,7 +892,7 @@ msgstr "Nom"
msgid "new"
msgstr "nouveau"
-#: src/components/ChatModal.tsx:1105
+#: src/components/ChatModal.tsx:1121
#: src/components/FeedTabBar.tsx:31
#: src/pages/UserPublicProfile.tsx:1294
msgid "New"
@@ -899,7 +916,7 @@ msgstr "Nouveau mot de passe"
msgid "New playlist"
msgstr "Nouvelle collection"
-#: src/components/DumpCreateModal.tsx:965
+#: src/components/DumpCreateModal.tsx:964
msgid "Next →"
msgstr "Suivant →"
@@ -921,7 +938,7 @@ msgstr "Aucune reco ne correspond à « {q} »."
msgid "No dumps yet. Be the first!"
msgstr "Pas encore de recos. Soyez le premier !"
-#: src/components/TextEditor.tsx:331
+#: src/components/TextEditor.tsx:643
msgid "No emoji found."
msgstr "Aucun emoji trouvé."
@@ -934,7 +951,7 @@ msgstr "Pas encore de collections suivies."
msgid "No invitees yet."
msgstr "Aucun invité pour le moment."
-#: src/components/ChatModal.tsx:1089
+#: src/components/ChatModal.tsx:1105
msgid "No messages yet. Say hello!"
msgstr "Aucun message pour l'instant. Dites bonjour !"
@@ -991,6 +1008,10 @@ msgstr "Désactivés"
msgid "On"
msgstr "Activés"
+#: src/components/TextEditor.tsx:338
+msgid "Only images can be dropped here."
+msgstr "Seules les images peuvent être déposées ici."
+
#: src/components/FileDropZone.tsx:134
msgid "or <0>browse files0>"
msgstr "ou <0>parcourir les fichiers0>"
@@ -1030,11 +1051,11 @@ msgstr "Collez un lien…"
msgid "Picked up where you left off."
msgstr "Reprise de votre brouillon."
-#: src/pages/PlaylistDetail.tsx:866
+#: src/pages/PlaylistDetail.tsx:870
msgid "Playlist title"
msgstr "Titre de la collection"
-#: src/components/AppHeader.tsx:160
+#: src/components/AppHeader.tsx:148
#: src/components/DumpCreateModal.tsx:68
#: src/components/UserMenu.tsx:62
#: src/pages/Search.tsx:177
@@ -1049,17 +1070,17 @@ msgstr "Collections"
msgid "Playlists ({0}{1})"
msgstr "Collections ({0}{1})"
-#: src/components/CommentThread.tsx:479
+#: src/components/CommentThread.tsx:492
msgid "Post comment"
msgstr "Publier le commentaire"
-#: src/components/CommentThread.tsx:401
+#: src/components/CommentThread.tsx:414
msgid "Post reply"
msgstr "Publier la réponse"
-#: src/components/CommentThread.tsx:402
-#: src/components/CommentThread.tsx:480
-#: src/components/DumpCreateModal.tsx:959
+#: src/components/CommentThread.tsx:415
+#: src/components/CommentThread.tsx:493
+#: src/components/DumpCreateModal.tsx:958
msgid "Posting…"
msgstr "Publication…"
@@ -1077,7 +1098,7 @@ msgid "private"
msgstr "privé"
#: src/components/form/SegmentedField.tsx:81
-#: src/pages/PlaylistDetail.tsx:905
+#: src/pages/PlaylistDetail.tsx:911
msgid "Private"
msgstr "Privé"
@@ -1087,7 +1108,7 @@ msgid "public"
msgstr "public"
#: src/components/form/SegmentedField.tsx:80
-#: src/pages/PlaylistDetail.tsx:898
+#: src/pages/PlaylistDetail.tsx:904
msgid "Public"
msgstr "Public"
@@ -1141,13 +1162,13 @@ msgstr "Retirer le vote"
msgid "Replace file"
msgstr "Remplacer le fichier"
-#: src/components/ChatModal.tsx:315
-#: src/components/ChatModal.tsx:316
-#: src/components/CommentThread.tsx:361
+#: src/components/ChatModal.tsx:326
+#: src/components/ChatModal.tsx:327
+#: src/components/CommentThread.tsx:374
msgid "Reply"
msgstr "Répondre"
-#: src/components/ChatModal.tsx:1240
+#: src/components/ChatModal.tsx:1256
msgid "Replying to"
msgstr "En réponse à"
@@ -1173,7 +1194,7 @@ msgstr "Réinitialiser par défaut"
msgid "Reset to the suggested title"
msgstr "Rétablir le titre suggéré"
-#: src/components/ChatModal.tsx:502
+#: src/components/ChatModal.tsx:519
#: src/pages/Dump.tsx:283
#: src/pages/DumpEdit.tsx:180
msgid "Retry"
@@ -1183,27 +1204,27 @@ msgstr "Réessayer"
msgid "Role"
msgstr "Rôle"
-#: src/components/ChatModal.tsx:221
-#: src/components/CommentThread.tsx:333
+#: src/components/ChatModal.tsx:232
+#: src/components/CommentThread.tsx:346
#: src/pages/Dump.tsx:394
-#: src/pages/DumpEdit.tsx:487
-#: src/pages/PlaylistDetail.tsx:930
+#: src/pages/DumpEdit.tsx:486
+#: src/pages/PlaylistDetail.tsx:936
#: src/pages/UserPublicProfile.tsx:1756
-#: src/pages/UserPublicProfile.tsx:1829
+#: src/pages/UserPublicProfile.tsx:1830
msgid "Save"
msgstr "Enregistrer"
#: src/components/ChangePasswordModal.tsx:103
-#: src/components/CommentThread.tsx:334
+#: src/components/CommentThread.tsx:347
#: src/pages/Dump.tsx:393
-#: src/pages/PlaylistDetail.tsx:926
+#: src/pages/PlaylistDetail.tsx:932
#: src/pages/ResetPassword.tsx:126
#: src/pages/UserPublicProfile.tsx:1753
-#: src/pages/UserPublicProfile.tsx:1826
+#: src/pages/UserPublicProfile.tsx:1827
msgid "Saving…"
msgstr "Enregistrement…"
-#: src/components/AppHeader.tsx:139
+#: src/components/AppHeader.tsx:127
#: src/components/SearchBar.tsx:73
#: src/pages/Search.tsx:50
msgid "Search"
@@ -1213,6 +1234,14 @@ msgstr "Rechercher"
msgid "Search dumps, users, playlists…"
msgstr "Rechercher des recos, utilisateurs, collections…"
+#: src/components/TextEditor.tsx:612
+msgid "Search emoji"
+msgstr "Rechercher un emoji"
+
+#: src/components/TextEditor.tsx:611
+msgid "Search emoji…"
+msgstr "Rechercher un emoji…"
+
#: src/pages/Search.tsx:218
msgid "Search failed"
msgstr "Recherche échouée"
@@ -1221,7 +1250,7 @@ msgstr "Recherche échouée"
msgid "Searching…"
msgstr "Recherche…"
-#: src/components/ChatModal.tsx:1280
+#: src/components/ChatModal.tsx:1296
msgid "Send"
msgstr "Envoyer"
@@ -1229,12 +1258,12 @@ msgstr "Envoyer"
msgid "Send reset link"
msgstr "Envoyer le lien de réinitialisation"
-#: src/components/ChatModal.tsx:520
+#: src/components/ChatModal.tsx:537
#: src/pages/UserLogin.tsx:142
msgid "Sending…"
msgstr "Envoi…"
-#: src/components/AppHeader.tsx:176
+#: src/components/AppHeader.tsx:164
msgid "Server unreachable"
msgstr "Serveur inaccessible"
@@ -1247,7 +1276,7 @@ msgstr "Définir un nouveau mot de passe"
msgid "Settings"
msgstr "Paramètres"
-#: src/components/ChatModal.tsx:1064
+#: src/components/ChatModal.tsx:1080
msgid "Several people are typing…"
msgstr "Plusieurs personnes écrivent…"
@@ -1259,6 +1288,10 @@ msgstr "Usage unique, valable 7 jours."
msgid "slug"
msgstr "identifiant"
+#: src/components/TextEditor.tsx:297
+msgid "Some images couldn't be uploaded."
+msgstr "Certaines images n'ont pas pu être envoyées."
+
#: src/components/form/FormError.tsx:15
#: src/components/PageError.tsx:13
#: src/contexts/WSProvider.tsx:663
@@ -1285,6 +1318,10 @@ msgstr "Lancer la recherche"
msgid "the chat"
msgstr "la tribune"
+#: src/components/TextEditor.tsx:298
+msgid "The image couldn't be uploaded."
+msgstr "L'image n'a pas pu être envoyée."
+
#: src/pages/UserRegister.tsx:97
msgid "This invite link is missing, expired, or already used."
msgstr "Ce lien d'invitation est manquant, expiré ou déjà utilisé."
@@ -1315,7 +1352,7 @@ msgstr "Titre"
msgid "Title is required."
msgstr "Un titre est requis."
-#: src/components/ChatModal.tsx:133
+#: src/components/ChatModal.tsx:147
#: src/pages/Notifications.tsx:398
msgid "Today"
msgstr "Aujourd'hui"
@@ -1325,11 +1362,11 @@ msgstr "Aujourd'hui"
msgid "Track {0}"
msgstr "Piste {0}"
-#: src/components/ChatModal.tsx:1081
+#: src/components/ChatModal.tsx:1097
msgid "Try again"
msgstr "Réessayer"
-#: src/components/ChatModal.tsx:1265
+#: src/components/ChatModal.tsx:1281
msgid "Type a message…"
msgstr "Écrivez un message…"
@@ -1354,7 +1391,11 @@ msgstr "Envoi échoué"
msgid "Uploading {0}%"
msgstr "Envoi {0} %"
-#: src/components/DumpCreateModal.tsx:962
+#: src/components/TextEditor.tsx:526
+msgid "Uploading image…"
+msgstr "Envoi de l'image…"
+
+#: src/components/DumpCreateModal.tsx:961
msgid "Uploading…"
msgstr "Envoi…"
@@ -1423,11 +1464,11 @@ msgid "What makes it worth it?"
msgstr "Pourquoi on en voudrait ?"
#: src/pages/UserPublicProfile.tsx:925
-#: src/pages/UserPublicProfile.tsx:1818
+#: src/pages/UserPublicProfile.tsx:1819
msgid "Who am I?"
msgstr "Qui suis-je ?"
-#: src/components/DumpCreateModal.tsx:928
+#: src/components/DumpCreateModal.tsx:927
msgid "Who can see it"
msgstr "Qui peut la voir"
@@ -1440,11 +1481,11 @@ msgstr "Pourquoi & où"
msgid "Why?"
msgstr "Pourquoi ?"
-#: src/components/CommentThread.tsx:405
+#: src/components/CommentThread.tsx:418
msgid "Write a reply…"
msgstr "Écrire une réponse…"
-#: src/components/ChatModal.tsx:136
+#: src/components/ChatModal.tsx:150
#: src/pages/Notifications.tsx:400
msgid "Yesterday"
msgstr "Hier"
diff --git a/src/pages/DumpEdit.tsx b/src/pages/DumpEdit.tsx
index 780d1ca8..0ec5fb3a 100644
--- a/src/pages/DumpEdit.tsx
+++ b/src/pages/DumpEdit.tsx
@@ -459,7 +459,6 @@ function DumpEditForm({
placeholder={t`What makes it worth it?`}
rows={3}
maxLength={VALIDATION.DUMP_COMMENT_MAX}
- rules={{ maxLength: VALIDATION.DUMP_COMMENT_MAX }}
/>
{
@@ -862,6 +863,9 @@ function PlaylistEditForm(
className="playlist-edit-input"
value={field.value}
onChange={field.onChange}
+ onKeyDown={(e) => {
+ if (e.key === "Escape") onCancel();
+ }}
autoFocus
placeholder={t`Playlist title`}
maxLength={VALIDATION.PLAYLIST_TITLE_MAX}
@@ -877,6 +881,8 @@ function PlaylistEditForm(
className="playlist-edit-textarea"
value={field.value}
onChange={field.onChange}
+ onSubmit={() => void onSubmit()}
+ onCancel={onCancel}
placeholder={t`Description (optional)`}
autoResize
rows={2}
@@ -924,8 +930,8 @@ function PlaylistEditForm(
Saving…}
- disabled={description.length >
- VALIDATION.PLAYLIST_DESCRIPTION_MAX}
+ disabled={!title.trim() ||
+ description.length > VALIDATION.PLAYLIST_DESCRIPTION_MAX}
>
Save
diff --git a/src/pages/UserPublicProfile.tsx b/src/pages/UserPublicProfile.tsx
index 72ba9cc3..9c441c10 100644
--- a/src/pages/UserPublicProfile.tsx
+++ b/src/pages/UserPublicProfile.tsx
@@ -1815,6 +1815,7 @@ function DescriptionEditor(
value={field.value}
onChange={field.onChange}
onSubmit={() => void onSubmit()}
+ onCancel={submitting ? undefined : onCancel}
placeholder={t`Who am I?`}
autoResize
maxLength={VALIDATION.USER_DESCRIPTION_MAX}
diff --git a/src/themes/brutalist.css b/src/themes/brutalist.css
index 327806f6..519f9137 100644
--- a/src/themes/brutalist.css
+++ b/src/themes/brutalist.css
@@ -94,6 +94,8 @@
[data-style="brutalist"] .vote-btn,
[data-style="brutalist"] .emoji-picker-float [frimousse-emoji],
[data-style="brutalist"] .emoji-picker-close-btn,
+[data-style="brutalist"] .text-editor-emoji-btn,
+[data-style="brutalist"] .emoji-picker-float [frimousse-skin-tone-selector],
[data-style="brutalist"] .new-item-toggle,
[data-style="brutalist"] .btn--ghost,
[data-style="brutalist"] .audio-player-btn,
@@ -128,6 +130,8 @@
[data-style="brutalist"] .vote-btn:hover:not(:disabled),
[data-style="brutalist"] .emoji-picker-float [frimousse-emoji]:hover,
[data-style="brutalist"] .emoji-picker-close-btn:hover,
+[data-style="brutalist"] .text-editor-emoji-btn:hover:not(:disabled),
+[data-style="brutalist"] .emoji-picker-float [frimousse-skin-tone-selector]:hover,
[data-style="brutalist"] .new-item-toggle:hover:not(:disabled),
[data-style="brutalist"] .btn--ghost:hover:not(:disabled),
[data-style="brutalist"] .audio-player-btn:hover:not(:disabled),
diff --git a/src/themes/geocities.css b/src/themes/geocities.css
index 602a00c4..0e0eea19 100644
--- a/src/themes/geocities.css
+++ b/src/themes/geocities.css
@@ -189,12 +189,6 @@
background: var(--color-surface);
}
-/* Ghost / icon-only buttons stay flat */
-[data-style="geocities"] .modal-close-btn,
-[data-style="geocities"] .playlist-remove-btn,
-[data-style="geocities"] .playlist-card-delete-btn,
-[data-style="geocities"] .emoji-picker-float [frimousse-emoji],
-[data-style="geocities"] .emoji-picker-close-btn,
/* Turn the tint up and give it hard diagonal bands — a flat wash reads as an
unstyled gap in a theme built on saturation. */
[data-style="geocities"] {
@@ -209,6 +203,14 @@
);
}
+/* Ghost / icon-only buttons stay flat */
+[data-style="geocities"] .modal-close-btn,
+[data-style="geocities"] .playlist-remove-btn,
+[data-style="geocities"] .playlist-card-delete-btn,
+[data-style="geocities"] .emoji-picker-float [frimousse-emoji],
+[data-style="geocities"] .emoji-picker-close-btn,
+[data-style="geocities"] .text-editor-emoji-btn,
+[data-style="geocities"] .emoji-picker-float [frimousse-skin-tone-selector],
[data-style="geocities"] .btn--ghost,
[data-style="geocities"] .new-item-toggle,
[data-style="geocities"] .audio-player-btn,
diff --git a/src/utils/charCount.ts b/src/utils/charCount.ts
index 35a424e9..649c7c84 100644
--- a/src/utils/charCount.ts
+++ b/src/utils/charCount.ts
@@ -3,3 +3,9 @@ export function charCountClass(len: number, max: number): string {
if (len >= max * 0.85) return " text-editor-count--warn";
return "";
}
+
+/** Whether `value` is past `max` — the counter's danger state past its
+ * edge, and what the server would refuse. */
+export function overLimit(value: string, max: number): boolean {
+ return value.length > max;
+}
diff --git a/src/utils/editorText.ts b/src/utils/editorText.ts
new file mode 100644
index 00000000..32f4a597
--- /dev/null
+++ b/src/utils/editorText.ts
@@ -0,0 +1,33 @@
+// Text helpers shared by the editor's autocompletes (mentions, emoji).
+
+/**
+ * Whether the caret at the end of `textBefore` sits inside markdown code — an
+ * open ``` fence, or an unclosed `span` on the current line — where `@bob`
+ * and `:smile` are literal text, not something to complete.
+ */
+export function insideCode(textBefore: string): boolean {
+ const fences = textBefore.match(/^\s{0,3}```/gm)?.length ?? 0;
+ if (fences % 2 === 1) return true;
+ const line = textBefore.slice(textBefore.lastIndexOf("\n") + 1);
+ if (/^\s{0,3}```/.test(line)) return false;
+ return (line.match(/`/g)?.length ?? 0) % 2 === 1;
+}
+
+const EMOJI_ONLY_RE =
+ /^(?:\p{Extended_Pictographic}|\p{Regional_Indicator}|[\u{1F3FB}-\u{1F3FF}#*0-9]|\u200D|\uFE0F|\u20E3|\s)+$/u;
+const PICTOGRAPH_RE = /\p{Extended_Pictographic}|\p{Regional_Indicator}/u;
+
+/** Whether `text` is nothing but a few emoji (at most `max`), for the
+ * jumbo rendering chats give a message like "🎉" or "👍👍". */
+export function isEmojiOnly(text: string, max = 3): boolean {
+ const trimmed = text.trim();
+ if (!trimmed || !EMOJI_ONLY_RE.test(trimmed)) return false;
+ if (!PICTOGRAPH_RE.test(trimmed)) return false; // "123", "#"
+ const segments = [
+ ...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(
+ trimmed.replace(/\s+/g, ""),
+ ),
+ ];
+ return segments.length <= max &&
+ segments.every((s) => PICTOGRAPH_RE.test(s.segment));
+}