diff --git a/API.md b/API.md index 493c3375..2575f1e5 100644 --- a/API.md +++ b/API.md @@ -171,6 +171,9 @@ thumbnail, cover or attachment takes the owner's media token as `?token=`. - `GET /api/chat?before=&after=` — signed in. A page of messages. The chat itself goes over `/ws`. +- A message from a moderator or an admin (`chat:everyone`) that says + `@everyone` notifies every user who doesn't have the chat open, and comes + flagged `mentionsEveryone`. No account may be named `everyone`. ### invites diff --git a/api/db/migrate.ts b/api/db/migrate.ts index 855daae1..c171e117 100644 --- a/api/db/migrate.ts +++ b/api/db/migrate.ts @@ -29,6 +29,7 @@ import { up as up0019UserSettings } from "./migrations/0019_user_settings.ts"; import { up as up0020DigestSchedule } from "./migrations/0020_digest_schedule.ts"; import { up as up0021PageUrls } from "./migrations/0021_page_urls.ts"; import { up as up0022ReserveCommunity } from "./migrations/0022_reserve_community.ts"; +import { up as up0023MentionEveryone } from "./migrations/0023_mention_everyone.ts"; interface Migration { name: string; @@ -70,6 +71,7 @@ const MIGRATIONS: Migration[] = [ { name: "0020_digest_schedule", up: up0020DigestSchedule }, { name: "0021_page_urls", up: up0021PageUrls }, { name: "0022_reserve_community", up: up0022ReserveCommunity }, + { name: "0023_mention_everyone", up: up0023MentionEveryone }, ]; /** diff --git a/api/db/migrations/0023_mention_everyone.ts b/api/db/migrations/0023_mention_everyone.ts new file mode 100644 index 00000000..9855c6fd --- /dev/null +++ b/api/db/migrations/0023_mention_everyone.ts @@ -0,0 +1,74 @@ +/** + * Prepares the chat's `@everyone`: it adds `chat_messages.mentions_everyone`, + * and frees the name `everyone`, which no account may have any more. + * + * @module + */ + +import type { DatabaseSync } from "node:sqlite"; +import { EVERYONE, mentionsEveryone } from "../../../shared/mention.ts"; +import { ROLE_PERMISSIONS } from "../../../shared/permissions.ts"; + +/** + * Run once by `runMigrations`. The messages that a moderator or an admin + * already sent with `@everyone` get the flag, so that editing one later + * doesn't notify everyone of an old message; none is notified now. A user + * named `everyone`, in any case, is renamed to the first free `everyone_2`, + * `everyone_3`…, with no alias left behind, and an alias `everyone` is + * dropped. Both are logged. + */ +export function up(db: DatabaseSync): void { + const cols = db.prepare(`PRAGMA table_info(chat_messages);`).all() as { + name: string; + }[]; + if (!cols.some((c) => c.name === "mentions_everyone")) { + db.exec( + `ALTER TABLE chat_messages ADD COLUMN mentions_everyone INTEGER NOT NULL DEFAULT 0;`, + ); + } + + const roles = Object.entries(ROLE_PERMISSIONS) + .filter(([, perms]) => perms.includes("chat:everyone")) + .map(([role]) => role); + const messages = db.prepare( + `SELECT m.id, m.body FROM chat_messages m JOIN users u ON u.id = m.user_id + WHERE m.mentions_everyone = 0 + AND u.role IN (${roles.map(() => "?").join(", ")}) + AND m.body LIKE '%@%';`, + ).all(...roles) as { id: string; body: string }[]; + const flag = db.prepare( + `UPDATE chat_messages SET mentions_everyone = 1 WHERE id = ?;`, + ); + for (const m of messages) { + if (mentionsEveryone(m.body)) flag.run(m.id); + } + + const taken = (name: string) => + !!db.prepare( + `SELECT 1 FROM users WHERE lower(username) = lower(?) + UNION ALL SELECT 1 FROM user_aliases WHERE lower(username) = lower(?);`, + ).get(name, name); + const user = db.prepare( + `SELECT id, username FROM users WHERE lower(username) = ?;`, + ).get(EVERYONE) as { id: string; username: string } | undefined; + if (user) { + let n = 2; + while (taken(`${EVERYONE}_${n}`)) n++; + const renamed = `${EVERYONE}_${n}`; + db.prepare(`UPDATE users SET username = ? WHERE id = ?;`).run( + renamed, + user.id, + ); + console.log( + `[migrate] user "${user.username}" is now "${renamed}": @${EVERYONE} is reserved`, + ); + } + const alias = db.prepare( + `DELETE FROM user_aliases WHERE lower(username) = ? RETURNING user_id;`, + ).get(EVERYONE) as { user_id: string } | undefined; + if (alias) { + console.log( + `[migrate] the former name "${EVERYONE}" of user ${alias.user_id} is dropped: @${EVERYONE} is reserved`, + ); + } +} diff --git a/api/db/schema.sql b/api/db/schema.sql index f6104e08..f4cdd1cd 100644 --- a/api/db/schema.sql +++ b/api/db/schema.sql @@ -265,6 +265,7 @@ CREATE TABLE chat_messages ( created_at TEXT NOT NULL, updated_at TEXT, reply_to_id TEXT, + mentions_everyone INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX idx_chat_messages_created_id ON chat_messages(created_at, id); diff --git a/api/mail/digest.ts b/api/mail/digest.ts index 31da5de6..06db66bc 100644 --- a/api/mail/digest.ts +++ b/api/mail/digest.ts @@ -35,6 +35,7 @@ export type HighlightKind = | "mentionDump" | "mentionPlaylist" | "mentionChat" + | "mentionEveryone" | "comment" | "playlistFollow" | "userFollow" @@ -158,6 +159,8 @@ function highlightSentence( return i18n._("email.digest.hl.mentionIn", values); case "mentionPlaylist": return i18n._("email.digest.hl.mentionPlaylist", values); + case "mentionEveryone": + return i18n._("email.digest.hl.mentionEveryone", values); case "comment": return i18n._("email.digest.hl.comment", values); case "playlistFollow": @@ -244,7 +247,7 @@ export function renderDigestEmail( ) .join(" · "), activity.highlights.map((h) => { - const target = h.kind === "mentionChat" + const target = h.kind === "mentionChat" || h.kind === "mentionEveryone" ? mdLink( i18n._("email.digest.theChat"), content.links.chat, diff --git a/api/model/chat.ts b/api/model/chat.ts index ef1c6cb1..7912f0b6 100644 --- a/api/model/chat.ts +++ b/api/model/chat.ts @@ -26,6 +26,8 @@ export interface ChatMessageRow { reply_to_author: string | null; /** Cut to chat-service's `REPLY_SNIPPET_LEN` characters. */ reply_to_body: string | null; + /** 1 or 0. */ + mentions_everyone: number; [key: string]: SQLOutputValue; } @@ -50,7 +52,8 @@ export function isChatMessageRow(obj: unknown): obj is ChatMessageRow { "reply_to_author" in obj && (typeof obj.reply_to_author === "string" || obj.reply_to_author === null) && "reply_to_body" in obj && - (typeof obj.reply_to_body === "string" || obj.reply_to_body === null); + (typeof obj.reply_to_body === "string" || obj.reply_to_body === null) && + "mentions_everyone" in obj && typeof obj.mentions_everyone === "number"; } /** @@ -69,5 +72,6 @@ export function chatMessageRowToApi(row: ChatMessageRow): ChatMessage { replyToId: row.reply_to_id ?? undefined, replyToAuthor: row.reply_to_author ?? undefined, replyToBody: row.reply_to_body ?? undefined, + mentionsEveryone: row.mentions_everyone === 1 || undefined, }; } diff --git a/api/routes/ws.ts b/api/routes/ws.ts index baef7cdc..bf5facda 100644 --- a/api/routes/ws.ts +++ b/api/routes/ws.ts @@ -34,6 +34,7 @@ import { type WsClient, } from "../services/ws-service.ts"; import { + chatMessageMentionsEveryone, createChatMessage, deleteChatMessage, getChatReadState, @@ -42,6 +43,7 @@ import { } from "../services/chat-service.ts"; import { getUnreadCount, + notifyEveryone, notifyMentions, } from "../services/notification-service.ts"; import { hasPermission } from "../../shared/permissions.ts"; @@ -369,7 +371,8 @@ function optionalString(value: unknown, maxLength: number): string | undefined { /** * Handles `chat_send`. It stores the message, broadcasts it, and notifies the - * @mentioned users who aren't reading the chat. + * @mentioned users who aren't reading the chat, and everyone who isn't when + * the message is flagged `mentionsEveryone`. */ function handleChatSend( client: WsClient, @@ -404,6 +407,9 @@ function handleChatSend( undefined, isUserChatOpen, ); + if (message.mentionsEveryone) { + notifyEveryone(userId, message.id, isUserChatOpen); + } } catch (err) { sendChatError( client, @@ -430,7 +436,8 @@ function canModerateChat(client: WsClient): boolean { /** * Handles `chat_edit`. It stores the new body, broadcasts it, and notifies, in - * the author's name, the users the edit newly mentions. + * the author's name, the users the edit newly mentions, and everyone when the + * edit adds a `@everyone` the author may say. */ function handleChatEdit(client: WsClient, id: unknown, body: unknown): void { const userId = chatWriter(client); @@ -441,6 +448,10 @@ function handleChatEdit(client: WsClient, id: unknown, body: unknown): void { } try { + // Only an edit that adds `@everyone` notifies everyone: fixing a typo in + // an announcement mustn't reach those who had the chat open when it was + // sent, nor those who joined since. + const wasEveryone = chatMessageMentionsEveryone(id); const message = updateChatMessage( id, body, @@ -460,6 +471,9 @@ function handleChatEdit(client: WsClient, id: unknown, body: unknown): void { undefined, isUserChatOpen, ); + if (message.mentionsEveryone && !wasEveryone) { + notifyEveryone(message.userId, message.id, isUserChatOpen); + } } catch (err) { sendChatError( client, diff --git a/api/services/chat-service.ts b/api/services/chat-service.ts index 5595e4fc..bbc66add 100644 --- a/api/services/chat-service.ts +++ b/api/services/chat-service.ts @@ -14,6 +14,9 @@ import { chatMessageRowToApi, isChatMessageRow } from "../model/chat.ts"; import { db } from "../db/connection.ts"; import { linkAttachments } from "./attachment-service.ts"; import { VALIDATION } from "../../shared/validation.ts"; +import { mentionsEveryone } from "../../shared/mention.ts"; +import { hasPermission } from "../../shared/permissions.ts"; +import { isRole } from "../../shared/user.ts"; const DEFAULT_LIMIT = 50; const MAX_LIMIT = 100; @@ -28,7 +31,7 @@ const REPLY_SNIPPET_LEN = 300; /** The columns of a `ChatMessageRow`, to select over {@link FROM_JOINS}. */ const SELECT_COLS = `m.id, m.user_id, m.body, m.created_at, m.updated_at, m.reply_to_id, - u.username as author_username, u.avatar_mime as author_avatar_mime, + m.mentions_everyone, u.username as author_username, u.avatar_mime as author_avatar_mime, ru.username as reply_to_author, substr(rm.body, 1, ${REPLY_SNIPPET_LEN}) as reply_to_body`; /** @@ -51,10 +54,27 @@ function fetchMessage(id: string): ChatMessage { return chatMessageRowToApi(row); } +/** + * Returns 1 when `body`, written by `authorId`, reaches everyone: it says + * `@everyone` and the author's role, read now, has `chat:everyone`. Else 0, + * for `chat_messages.mentions_everyone`. + */ +function everyoneFlag(authorId: string, body: string): number { + if (!mentionsEveryone(body)) return 0; + const row = db.prepare(`SELECT role FROM users WHERE id = ?;`).get( + authorId, + ) as { role: unknown } | undefined; + return row && isRole(row.role) && hasPermission(row.role, "chat:everyone") + ? 1 + : 0; +} + /** * Stores a message from `userId` on a `chat_send`, and claims the pasted images * it links to. A `replyToId` naming no message is dropped, and the message is - * posted as no reply. The caller broadcasts the result. + * posted as no reply. The message `mentionsEveryone` when it says `@everyone` + * and `userId` may. The caller broadcasts the result, and notifies everyone + * when it is flagged. * * @throws {APIException} 400 when the body is blank or longer than * `VALIDATION.CHAT_MESSAGE_MAX`. @@ -84,8 +104,9 @@ export function createChatMessage( const id = crypto.randomUUID(); const createdAt = new Date().toISOString(); db.prepare( - `INSERT INTO chat_messages (id, user_id, body, created_at, reply_to_id) VALUES (?, ?, ?, ?, ?);`, - ).run(id, userId, trimmed, createdAt, replyTo); + `INSERT INTO chat_messages (id, user_id, body, created_at, reply_to_id, mentions_everyone) + VALUES (?, ?, ?, ?, ?, ?);`, + ).run(id, userId, trimmed, createdAt, replyTo, everyoneFlag(userId, trimmed)); // Unclaimed attachments are deleted as orphans at startup. linkAttachments(trimmed, "chat", id); return fetchMessage(id); @@ -102,10 +123,24 @@ function getMessageOwner(id: string): string { return row.user_id; } +/** + * Checks whether message `id` is flagged `mentionsEveryone`, false when there + * is no such message. The WebSocket route reads it before an edit, to notify + * everyone only when the edit adds `@everyone`. + */ +export function chatMessageMentionsEveryone(id: string): boolean { + const row = db.prepare( + `SELECT mentions_everyone FROM chat_messages WHERE id = ?;`, + ).get(id) as { mentions_everyone: number } | undefined; + return row?.mentions_everyone === 1; +} + /** * Replaces a message's body on a `chat_edit`, and claims any image the new body - * links to. `canModerate` lets a moderator edit anyone's message. The caller - * broadcasts the result. + * links to. `canModerate` lets a moderator edit anyone's message. The flag + * `mentionsEveryone` is set again from the new body and the author's role, not + * the editor's: a moderator's edit can't make a user's message reach everyone. + * The caller broadcasts the result. * * @throws {APIException} 404 when there is no such message, 403 when the user * is neither its author nor a moderator, 400 when the body is blank or too @@ -131,8 +166,10 @@ export function updateChatMessage( `Message exceeds ${VALIDATION.CHAT_MESSAGE_MAX} characters`, ); } - db.prepare(`UPDATE chat_messages SET body = ?, updated_at = ? WHERE id = ?;`) - .run(trimmed, new Date().toISOString(), id); + db.prepare( + `UPDATE chat_messages SET body = ?, updated_at = ?, mentions_everyone = ? + WHERE id = ?;`, + ).run(trimmed, new Date().toISOString(), everyoneFlag(ownerId, trimmed), id); linkAttachments(trimmed, "chat", id); return fetchMessage(id); } diff --git a/api/services/digest-service.ts b/api/services/digest-service.ts index 61e7c941..3f10391d 100644 --- a/api/services/digest-service.ts +++ b/api/services/digest-service.ts @@ -263,7 +263,7 @@ function highlightOf( }; case "chat": return { - kind: "mentionChat", + kind: d.everyone ? "mentionEveryone" : "mentionChat", who: d.mentionerUsername, quote: quoteOf( (db.prepare(`SELECT body FROM chat_messages WHERE id = ?;`) diff --git a/api/services/notification-service.ts b/api/services/notification-service.ts index 87497b84..6fd86a8f 100644 --- a/api/services/notification-service.ts +++ b/api/services/notification-service.ts @@ -379,6 +379,50 @@ export function notifyMentions( } } +/** + * Notifies every user but its author of chat message `messageId`, which says + * `@everyone`. The WebSocket route calls it, after {@link notifyMentions} so + * that a user also named is told by name, when the message is sent flagged + * `mentionsEveryone`, or when an edit flags it. Each user is notified once per + * message, however often it is edited. `skipUserId` spares the users who have + * the chat open, as for a mention. + */ +export function notifyEveryone( + mentionerUserId: string, + messageId: string, + skipUserId?: (userId: string) => boolean, +): void { + const mentionerRow = db.prepare( + `SELECT username FROM users WHERE id = ?;`, + ).get(mentionerUserId) as { username: string } | undefined; + if (!mentionerRow) return; + + const userRows = db.prepare(`SELECT id FROM users WHERE id != ?;`).all( + mentionerUserId, + ) as { id: string }[]; + + // One transaction for the fan-out, as for a dump's followers. + transaction(() => { + for (const row of userRows) { + if (skipUserId?.(row.id)) continue; + createNotification( + row.id, + "user_mentioned", + { + mentionerId: mentionerUserId, + mentionerUsername: mentionerRow.username, + contextType: "chat", + contextId: messageId, + contextTitle: "", + everyone: true, + }, + // The key of a mention by name, so a user named too gets one. + `mention:chat:${messageId}:${row.id}`, + ); + } + }); +} + /** * Notifies a dump's poster that `commenterId` commented on it, unless they are * the poster. `createComment` calls it for replies too, but not on an edit. diff --git a/api/services/user-service.ts b/api/services/user-service.ts index bd8f0b50..6ddb4e6b 100644 --- a/api/services/user-service.ts +++ b/api/services/user-service.ts @@ -33,6 +33,7 @@ import { parseRows } from "../model/rows.ts"; import { isEmailEnabled, sendEmail } from "./email-service.ts"; import { renderWelcomeEmail } from "../mail/welcome.ts"; import { USERNAME_RE, VALIDATION } from "../../shared/validation.ts"; +import { EVERYONE } from "../../shared/mention.ts"; /** Selects full `UserRow`s from `users u`, with the inviter's name joined. */ const USER_SELECT = @@ -43,13 +44,17 @@ const USER_SELECT = /** * Throws a 409 when `username` is, in any case, the name or a former name of - * an account other than `exceptUserId`. A former name stays its user's for - * good, so that old links and old @mentions can't be taken over. + * an account other than `exceptUserId`, or `everyone`, which the chat's + * `@everyone` takes. A former name stays its user's for good, so that old + * links and old @mentions can't be taken over. */ function assertUsernameAvailable( username: string, exceptUserId?: string, ): void { + if (username.toLowerCase() === EVERYONE) { + throw new APIException(409, "Username already exists"); + } const row = db.prepare( `SELECT id FROM users WHERE lower(username) = lower(?) AND id != ? UNION ALL diff --git a/api/test/db/migrate.test.ts b/api/test/db/migrate.test.ts index 988d26dd..765969f1 100644 --- a/api/test/db/migrate.test.ts +++ b/api/test/db/migrate.test.ts @@ -28,14 +28,14 @@ Deno.test("runMigrations: a fresh database is left as schema.sql made it", () => .get() as { n: number; }; - assertEquals(applied.n, 22); + assertEquals(applied.n, 23); // Running again applies nothing. runMigrations(db); assertEquals( (db.prepare(`SELECT COUNT(*) as n FROM schema_migrations;`).get() as { n: number; }).n, - 22, + 23, ); db.close(); }); diff --git a/api/test/db/migrations/0023_mention_everyone.test.ts b/api/test/db/migrations/0023_mention_everyone.test.ts new file mode 100644 index 00000000..d33864a8 --- /dev/null +++ b/api/test/db/migrations/0023_mention_everyone.test.ts @@ -0,0 +1,39 @@ +import "../../setup.ts"; +import { assertEquals } from "@std/assert"; +import { DatabaseSync } from "node:sqlite"; +import { up as up0023 } from "../../../db/migrations/0023_mention_everyone.ts"; + +Deno.test("migration 0023 flags past @everyone and frees the name", () => { + const db = new DatabaseSync(":memory:"); + db.exec(` + CREATE TABLE users (id TEXT PRIMARY KEY, username TEXT, role TEXT); + CREATE TABLE user_aliases (username TEXT PRIMARY KEY, user_id TEXT); + CREATE TABLE chat_messages (id TEXT PRIMARY KEY, user_id TEXT, body TEXT); + INSERT INTO users VALUES + ('m', 'mod', 'moderator'), ('u', 'Everyone', 'user'), + ('t', 'everyone_2', 'user'), ('a', 'ann', 'user'); + INSERT INTO user_aliases VALUES ('everyone_3', 't'), ('EVERYONE', 'a'); + INSERT INTO chat_messages VALUES + ('1', 'm', 'hi @everyone'), ('3', 'u', 'hi @everyone'), + ('4', 'm', 'hi all'); + `); + // In code, it says nothing. + db.prepare(`INSERT INTO chat_messages VALUES ('2', 'm', ?);`).run( + "hi `@everyone`", + ); + up0023(db); + up0023(db); // idempotent + + const flagged = db.prepare( + `SELECT id FROM chat_messages WHERE mentions_everyone = 1;`, + ).all() as { id: string }[]; + assertEquals(flagged.map((r) => r.id), ["1"]); + const names = db.prepare(`SELECT id, username FROM users ORDER BY id;`) + .all() as { id: string; username: string }[]; + assertEquals(names.find((r) => r.id === "u")?.username, "everyone_4"); + const aliases = db.prepare(`SELECT username FROM user_aliases;`).all() as { + username: string; + }[]; + assertEquals(aliases.map((r) => r.username), ["everyone_3"]); + db.close(); +}); diff --git a/api/test/routes/users.test.ts b/api/test/routes/users.test.ts index de86522a..bfd63722 100644 --- a/api/test/routes/users.test.ts +++ b/api/test/routes/users.test.ts @@ -669,6 +669,19 @@ test("renaming: the old name redirects for good, and nobody else can take it", a assertEquals((await rename(other.id, old)).status, 409); // Nor to a current one. assertEquals((await rename(other.id, renamed)).status, 409); + // Nor to everyone, in any case, which the chat's @everyone takes. + assertEquals((await rename(other.id, "Everyone")).status, 409); + assertEquals( + (await post("/api/users", { + json: { + username: "everyone", + password: PASSWORD, + email: `${uniqueName()}@example.test`, + inviteToken: invite(other.id), + }, + })).status, + 409, + ); // Signing in takes the current name only. assertEquals( (await post("/api/sessions", { diff --git a/api/test/routes/ws.test.ts b/api/test/routes/ws.test.ts index 4b65920a..c6647433 100644 --- a/api/test/routes/ws.test.ts +++ b/api/test/routes/ws.test.ts @@ -448,6 +448,73 @@ wsTest( }, ); +wsTest( + "chat: @everyone notifies all who aren't reading, when its author may say it", + async (connect) => { + const mod = await createUser({ role: "moderator" }); + const user = await createUser(); + const named = await createUser(); + const reader = await createUser(); + const m = await connect(mod); + const u = await connect(user); + const r = await connect(reader); + r.send({ type: "chat_focus", open: true }); + await r.sync(); + const mentioned = () => + Object.fromEntries( + (db.prepare( + `SELECT user_id, data FROM notifications WHERE type = 'user_mentioned';`, + ).all() as { user_id: string; data: string }[]) + .map((row) => [row.user_id, !!JSON.parse(row.data).everyone]), + ); + + // A user's @everyone reaches no one. + u.send({ type: "chat_send", body: "@everyone hi" }); + const plain = (await m.next("chat_message")).message; + assertEquals(plain.mentionsEveryone, undefined); + assertEquals(mentioned(), {}); + + // A moderator's reaches all but its author and the reader, and a user + // named too is told by name. + m.send({ + type: "chat_send", + body: `@Everyone, and @${named.username}`, + }); + const byMod = (x: { message: { userId: string } }) => + x.message.userId === mod.id; + const sent = (await u.next("chat_message", byMod)).message; + assertEquals(sent.mentionsEveryone, true); + const expected = { [user.id]: true, [named.id]: false }; + assertEquals(mentioned(), expected); + + // Fixing a typo reaches no one new, not even a newcomer. + const late = await createUser(); + m.send({ type: "chat_edit", id: sent.id, body: "@everyone, and all" }); + await u.next("chat_message_updated"); + assertEquals(mentioned(), expected); + + // A moderator's edit can't make a user's message reach everyone. + m.send({ type: "chat_edit", id: plain.id, body: "@everyone edited" }); + const edited = (await u.next("chat_message_updated")).message; + assertEquals(edited.mentionsEveryone, undefined); + assertEquals(mentioned(), expected); + + // An edit that adds it notifies, the newcomer included. + m.send({ type: "chat_send", body: "soon" }); + const draft = (await u.next("chat_message", byMod)).message; + m.send({ type: "chat_edit", id: draft.id, body: "now, @everyone" }); + await u.next("chat_message_updated"); + assertEquals( + count( + `SELECT COUNT(*) AS n FROM notifications WHERE data LIKE ?;`, + `%${draft.id}%`, + ), + 3, + ); + assertEquals(mentioned()[late.id], true); + }, +); + wsTest("chat: typing and read markers", async (connect) => { const alice = await createUser(); const bob = await createUser(); diff --git a/api/test/services/chat-service.test.ts b/api/test/services/chat-service.test.ts index a2fa23d7..8afcf2f5 100644 --- a/api/test/services/chat-service.test.ts +++ b/api/test/services/chat-service.test.ts @@ -1,6 +1,7 @@ import { createUser, db, test } from "../helpers.ts"; import { assertEquals, assertThrows } from "@std/assert"; import { + chatMessageMentionsEveryone, createChatMessage, deleteChatMessage, getChatMessagesAfter, @@ -161,3 +162,26 @@ test("read state: with no messages at all the marker can't be moved", async () = insertMessage(other.id, "later", T(5)); assertEquals(getChatReadState(me.id).unread, 1); }); + +test("mentionsEveryone: set from the author's role when the body is written", async () => { + const mod = await createUser({ role: "moderator" }); + const user = await createUser(); + const flagged = createChatMessage(mod.id, "hi @everyone"); + assertEquals(flagged.mentionsEveryone, true); + assertEquals(chatMessageMentionsEveryone(flagged.id), true); + assertEquals( + createChatMessage(user.id, "hi @everyone").mentionsEveryone, + undefined, + ); + assertEquals( + createChatMessage(mod.id, "`@everyone`").mentionsEveryone, + undefined, + ); + assertEquals(chatMessageMentionsEveryone(crypto.randomUUID()), false); + + // Demoted, the author's next edit loses it. + db.prepare(`UPDATE users SET role = 'user' WHERE id = ?;`).run(mod.id); + const edited = updateChatMessage(flagged.id, "hi @everyone!", mod.id, false); + assertEquals(edited.mentionsEveryone, undefined); + assertEquals(chatMessageMentionsEveryone(flagged.id), false); +}); diff --git a/locales/en.mjs b/locales/en.mjs index a96443c1..772d1a0a 100644 --- a/locales/en.mjs +++ b/locales/en.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"-K9EZb\":[\"Add email…\"],\"-Ya-b9\":[\"Failed to save\"],\"-iDCtG\":[\"Your settings\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Search dumps, users, playlists…\"],\"1QfxQT\":[\"Dismiss\"],\"1YKhJ3\":[\"1 new message\"],\"1njn7W\":[\"Light\"],\"1utXA6\":[\"Dumps\"],\"26iNma\":[\"Post comment\"],\"2DoBvq\":[\"Feeds\"],\"2Hlmdt\":[\"Write a reply…\"],\"2_DHgA\":[\"URL slug\"],\"2nrsEn\":[\"deleted message\"],\"2wxgft\":[\"Rename\"],\"2ygf_L\":[\"← Back\"],\"3KKSM4\":[\"private\"],\"3QuNUr\":[\"Several people are typing…\"],\"3SEwY1\":[\"Delete category \\\"\",[\"0\"],\"\\\"? This cannot be undone.\"],\"3qkggm\":[\"Fullscreen\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" followed your playlist <1>\",[\"1\"],\"\"],\"48e2R8\":[\"Edit email\"],\"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…\"],\"5NZ3tg\":[\"Turning off…\"],\"5TviPn\":[\"Cancel search\"],\"5cC8f2\":[\"Edited \",[\"0\"]],\"5oD9f_\":[\"Earlier\"],\"6Qly-0\":[\"a comment\"],\"6YE0P6\":[\"Editing \",[\"0\"]],\"6YtxFj\":[\"Name\"],\"6_dCYd\":[\"Overview\"],\"6gRgw8\":[\"Retry\"],\"7PHCIN\":[\"Cancel removal\"],\"7PzzBU\":[\"User\"],\"7d1a0d\":[\"Public\"],\"7noMWp\":[\"Nothing happened this week.\"],\"7sNhEz\":[\"Username\"],\"8-8abB\":[\"Disconnected — reconnecting…\"],\"8F1i42\":[\"Page not found\"],\"8JplJJ\":[\"Unsubscribed\"],\"8Ug9jB\":[\"Related\"],\"8ZsakT\":[\"Password\"],\"92o1-U\":[\"Failed to delete\"],\"9BruTc\":[\"Why?\"],\"9l4qcT\":[\"Drop a replacement here\"],\"9uI_rE\":[\"Undo\"],\"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\"],\"AhNP40\":[\"Seek\"],\"AnRu_j\":[\"Cancel reply\"],\"B4LeYP\":[\"Delete failed (\",[\"0\"],\")\"],\"BjitBo\":[\"Download \",[\"0\"]],\"BlpJ2G\":[\"Picked up where you left off.\"],\"CI50ct\":[\"Upvoted\"],\"C_qQE4\":[\"Already dumped by \",[\"0\"],\" \",[\"1\"]],\"Cf_gxi\":[\"Turn off the weekly digest?\"],\"Cj24wt\":[\"Registering…\"],\"CnqayG\":[\"Couldn't update. Try again.\"],\"D8DRYV\":[\"Delete this message?\"],\"DI3NBO\":[\"Open on \",[\"0\"]],\"DPfwMq\":[\"Done\"],\"DdeHXH\":[\"Delete this dump? This cannot be undone.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" upvoted <1>\",[\"1\"],\"\"],\"E417RG\":[\"You won't get the weekly digest any more. You can turn it back on in your settings.\"],\"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.\"],\"EwVrWF\":[\"The dump couldn't be removed. Try again.\"],\"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\"],\"Gv5nA5\":[\"Unexpected response format\"],\"H4MGkI\":[\"Expand player\"],\"H4o4sk\":[\"No one yet.\"],\"H8pzW-\":[\"Failed to update avatar\"],\"HTLDA4\":[\"Loading more…\"],\"H_PnT8\":[\"Insert emoji\"],\"I-x669\":[\"Invitees\"],\"IF7RiR\":[\"Cover\"],\"IO9KL_\":[\"Next track\"],\"IZX7TO\":[\"Failed to generate invite\"],\"IagCbF\":[\"URL\"],\"IgrLD_\":[\"Pause\"],\"IhK90M\":[\"Letters, numbers and underscores only\"],\"ImOQa9\":[\"Reply\"],\"JJ-Bhk\":[\"Your email address\"],\"JLasL-\":[\"Couldn't unsubscribe\"],\"JRQitQ\":[\"Confirm new password\"],\"JXr41k\":[\"<0>\",[\"0\"],\" commented on <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…\"],\"MHrjPM\":[\"Title\"],\"MKEPCY\":[\"Follow\"],\"MTb2Ot\":[\"No messages yet. Say hello!\"],\"Mj6oCJ\":[[\"0\"],\" is typing…\"],\"N40H-G\":[\"All\"],\"N6j2JH\":[\"Edit \",[\"0\"]],\"NUrY9o\":[\"Categories\"],\"Nu4oKW\":[\"Description\"],\"O32mmL\":[\"Edit description\"],\"O3oNi5\":[\"Email\"],\"Oprv1v\":[\"Password (min. \",[\"0\"],\" characters)\"],\"P5MuiB\":[\"Add image\"],\"PJTy3N\":[[\"totalUnread\",\"plural\",{\"one\":[\"#\",\" new\"],\"other\":[\"#\",\" 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 files\"],\"QLtPBd\":[\"No dumps in this playlist yet.\"],\"QctOgc\":[\"Could not connect to the server. Check your connection and try again.\"],\"R7SsBE\":[\"Mute\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Delete this playlist? This cannot be undone.\"],\"RTksSy\":[\"<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…\"],\"SMaFdc\":[\"Unsubscribe\"],\"Sad2tK\":[\"Sending…\"],\"So7gCD\":[\"Paste a link…\"],\"StovX6\":[\"This page does not exist.\"],\"Sxm8rQ\":[\"Users\"],\"T8ejtZ\":[\"This comment couldn't be deleted. Try again.\"],\"T91vKp\":[\"Play\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" was added to <1>\",[\"1\"],\"\"],\"TAtbho\":[\"Loading messages…\"],\"TN382O\":[\"Invalid link\"],\"Tv9vbB\":[\"Follow playlist\"],\"Tz0i8g\":[\"Settings\"],\"U3pytU\":[\"Admin\"],\"UNMVei\":[\"Forgot password?\"],\"UOZith\":[\"Failed to post\"],\"URAieT\":[\"Log in to vote\"],\"V38P77\":[\"Only its poster or a moderator can edit this dump.\"],\"VCoEm-\":[\"Back to login\"],\"VKsaTi\":[\"Replying to\"],\"VLAd8f\":[\"Max \",[\"limit\"]],\"V_e7nf\":[\"Set new password\"],\"VnNJbN\":[\"From playlists\"],\"VskHIx\":[\"Delete category\"],\"VyTYmS\":[\"Change avatar\"],\"W5A0Ly\":[\"An unexpected error occurred.\"],\"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\"],\"XnL-Eu\":[\"No users match \\\"\",[\"q\"],\"\\\".\"],\"Xs2Lez\":[\"This reset link is missing or malformed.\"],\"YK1Dhc\":[\"a post\"],\"YWnSuq\":[\"This unsubscribe link is missing or malformed.\"],\"YZxXK8\":[\"Close emoji picker\"],\"YcjhhX\":[\"User management\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" liked your comment on <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…\"],\"_I1_HN\":[\"Lowercase letters, numbers and single hyphens only\"],\"_R_sGB\":[\"Password changed successfully.\"],\"_aept4\":[\"Post reply\"],\"_cF7Rs\":[\"Volume\"],\"_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…\"],\"b_1P1D\":[\"Renaming…\"],\"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\"],\"digest.community\":[\"Community\"],\"digest.count.comments\":[[\"n\",\"plural\",{\"one\":[\"comment\"],\"other\":[\"comments\"]}]],\"digest.count.upvotes\":[[\"n\",\"plural\",{\"one\":[\"upvote\"],\"other\":[\"upvotes\"]}]],\"digest.invitedBy\":[\"invited by @\",[\"username\"]],\"digest.invitedByYou\":[\"invited by you\"],\"digest.monthly\":[\"Monthly recap\"],\"digest.monthly.off\":[\"Turn off monthly recaps\"],\"digest.newMembers\":[\"New members\"],\"digest.newPlaylists\":[\"New public playlists\"],\"digest.stat.chat\":[[\"n\",\"plural\",{\"one\":[\"chat message\"],\"other\":[\"chat messages\"]}]],\"digest.stat.dumps\":[[\"n\",\"plural\",{\"one\":[\"dump\"],\"other\":[\"dumps\"]}]],\"digest.topMonth\":[\"Top this month\"],\"digest.topWeek\":[\"Top this week\"],\"digest.weekly\":[\"Weekly digest\"],\"digest.weekly.off\":[\"Turn off weekly digests\"],\"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.\"],\"email.digest.addedTo\":[\"added to \",[\"playlist\"]],\"email.digest.andMore\":[\"and \",[\"n\"],\" more\"],\"email.digest.by\":[\"by @\",[\"username\"]],\"email.digest.chatWeek\":[[\"n\",\"plural\",{\"one\":[\"#\",\" message this week\"],\"other\":[\"#\",\" messages this week\"]}]],\"email.digest.communityPage\":[\"See it all on the community page\"],\"email.digest.count.followers\":[[\"n\",\"plural\",{\"one\":[\"new follower\"],\"other\":[\"new followers\"]}]],\"email.digest.count.likes\":[[\"n\",\"plural\",{\"one\":[\"comment like\"],\"other\":[\"comment likes\"]}]],\"email.digest.count.mentions\":[[\"n\",\"plural\",{\"one\":[\"mention\"],\"other\":[\"mentions\"]}]],\"email.digest.count.playlistFollows\":[[\"n\",\"plural\",{\"one\":[\"playlist follow\"],\"other\":[\"playlist follows\"]}]],\"email.digest.forYou\":[\"For you\"],\"email.digest.hl.comment\":[[\"who\"],\" commented on your dump \",[\"target\"]],\"email.digest.hl.commentLike\":[[\"who\"],\" liked your comment on \",[\"target\"]],\"email.digest.hl.mentionComment\":[[\"who\"],\" mentioned you in a comment on \",[\"target\"]],\"email.digest.hl.mentionIn\":[[\"who\"],\" mentioned you in \",[\"target\"]],\"email.digest.hl.mentionPlaylist\":[[\"who\"],\" mentioned you in the playlist \",[\"target\"]],\"email.digest.hl.playlistFollow\":[[\"who\"],\" followed your playlist \",[\"target\"]],\"email.digest.hl.userFollow\":[[\"who\"],\" started following you\"],\"email.digest.inChat\":[\"In the chat\"],\"email.digest.monthly.reason\":[\"You're getting the monthly recap because it's on in your settings.\"],\"email.digest.monthly.subject\":[\"The month on \",[\"site\"],\", \",[\"range\"]],\"email.digest.monthly.title\":[\"The month on \",[\"site\"]],\"email.digest.more\":[[\"n\"],\" more on your Followed feed\"],\"email.digest.network\":[\"From people you follow\"],\"email.digest.openChat\":[\"Open the chat\"],\"email.digest.playlistMeta\":[\"by @\",[\"username\"],\" · \",[\"n\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}]],\"email.digest.pre.members\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new member\"],\"other\":[\"#\",\" new members\"]}]],\"email.digest.pre.network\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new dump from people you follow\"],\"other\":[\"#\",\" new dumps from people you follow\"]}]],\"email.digest.pre.playlists\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new playlist\"],\"other\":[\"#\",\" new playlists\"]}]],\"email.digest.pre.topMonth\":[[\"n\",\"plural\",{\"one\":[\"the most upvoted dump of the month\"],\"other\":[\"the \",\"#\",\" most upvoted dumps of the month\"]}]],\"email.digest.pre.topWeek\":[[\"n\",\"plural\",{\"one\":[\"the most upvoted dump of the week\"],\"other\":[\"the \",\"#\",\" most upvoted dumps of the week\"]}]],\"email.digest.seeAll\":[[\"n\",\"plural\",{\"one\":[\"See the notification\"],\"other\":[\"See all \",\"#\",\" notifications\"]}]],\"email.digest.theChat\":[\"the chat\"],\"email.digest.totals\":[[\"dumps\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}],\", \",[\"upvotes\",\"plural\",{\"one\":[\"#\",\" upvote\"],\"other\":[\"#\",\" upvotes\"]}],\" and \",[\"comments\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}],\".\"],\"email.digest.upload\":[\"upload\"],\"email.digest.weekly.reason\":[\"You're getting the weekly digest because it's on in your settings.\"],\"email.digest.weekly.subject\":[\"Your week on \",[\"site\"],\", \",[\"range\"]],\"email.digest.weekly.title\":[\"Your week, \",[\"username\"]],\"email.digest.yours\":[\"yours\"],\"email.reset.body\":[\"Someone asked to reset the password of your \",[\"site\"],\" account. The button below lets you choose a new one. It works once, for \",[\"hours\",\"plural\",{\"one\":[\"#\",\" hour\"],\"other\":[\"#\",\" hours\"]}],\".\"],\"email.reset.button\":[\"Reset password\"],\"email.reset.fallback\":[\"If the button doesn't work, paste this link into your browser:\"],\"email.reset.hi\":[\"Hi \",[\"username\"],\",\"],\"email.reset.ignore\":[\"If you didn't ask for this, ignore this email. Your password stays the same.\"],\"email.reset.preheader\":[\"The link works once, for \",[\"hours\",\"plural\",{\"one\":[\"#\",\" hour\"],\"other\":[\"#\",\" hours\"]}],\".\"],\"email.reset.reason\":[\"You're getting this email because a password reset was requested for this address.\"],\"email.reset.subject\":[\"Reset your \",[\"site\"],\" password\"],\"email.reset.title\":[\"Reset your password\"],\"email.settings\":[\"Settings\"],\"email.welcome.body\":[\"# Welcome to \",[\"site\"],\"!\\n\\nHi **\",[\"username\"],\"**,\\n\\nYour account has been created successfully. Welcome aboard!\"],\"email.welcome.button\":[\"Open \",[\"site\"]],\"email.welcome.digests\":[\"Once a week you'll get a digest of what happened here, and once a month a recap of the most upvoted dumps. You can turn either off in your settings.\"],\"email.welcome.preheader\":[\"Your account is ready.\"],\"email.welcome.reason\":[\"You're getting this email because an account was created on \",[\"site\"],\" with this address.\"],\"email.welcome.subject\":[\"Welcome to \",[\"site\"]],\"en9o7K\":[\"Failed to post comment\"],\"etFQQS\":[\"What makes it worth it?\"],\"f3cEDO\":[\"Change image\"],\"fC6mXb\":[\"Upvote\"],\"fI-mNw\":[\"Playlists\"],\"fgLNSM\":[\"Register\"],\"gANddk\":[\"Uploading…\"],\"gGx5tM\":[\"Editing\"],\"gIQQwD\":[\"Failed to load\"],\"gIpLuy\":[\"A link is required\"],\"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\"],\" posted <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\"],\"jTGs8A\":[\"This site doesn't send email, so no digest goes out.\"],\"jbernk\":[\"Loading profile…\"],\"jnrIbz\":[\"Upvoted (\",[\"0\"],\")\"],\"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\"],\"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\"],\"mDJo8W\":[\"Dumps (\",[\"0\"],\")\"],\"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\"]}]],\"oW3yYY\":[\"This month\"],\"ogtYkT\":[\"Password updated\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" mentioned you in <1>\",[\"where\"],\"\"],\"pSheLH\":[\"No invitees yet.\"],\"pd3FCq\":[\"That file is over \",[\"limit\"],\".\"],\"pvnfJD\":[\"Dark\"],\"qIMfNQ\":[\"Delete playlist\"],\"qItrqH\":[\"You won't get the monthly recap any more. You can turn it back on in your settings.\"],\"qbDAcy\":[\"Dump it\"],\"qgx_78\":[\"Follow some public playlists to see their dumps here.\"],\"qnUJBI\":[\"The new order couldn't be saved. Try again.\"],\"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\"],\"rn6SBY\":[\"Unmute\"],\"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\"],\"tyiiHe\":[\"Nothing happened this month.\"],\"u1lDX2\":[\"Fetching preview…\"],\"uD0qXQ\":[\"Drop a file here\"],\"uISnsY\":[\"Could not delete\"],\"uMGUnV\":[\"No playlists yet.\"],\"ub1EEL\":[\"edited \",[\"0\"]],\"uomLas\":[\"Add category\"],\"v-Wp--\":[\"Open source\"],\"vJBF1r\":[\"Posting…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" unread)\"],\"vQMkHu\":[\"Remove vote\"],\"vuosjb\":[\"User menu\"],\"wckWOP\":[\"Manage\"],\"wixIgH\":[\"Already have an account? <0>Log in\"],\"wtxFp4\":[\"File too large (max \",[\"max\"],\").\"],\"x6tjuK\":[\"Default tab\"],\"xEWkgZ\":[\"← Back to all dumps\"],\"xPHtx0\":[\"Submit search\"],\"xVuNgt\":[\"+ New playlist\"],\"xc9O_u\":[\"Delete dump\"],\"y1G_DI\":[\"Could not rename\"],\"y6sq5j\":[\"Following\"],\"yA_6BX\":[\"View all →\"],\"yBBtRm\":[\"Follow some users to see their dumps here.\"],\"yByRxz\":[\"This week\"],\"yQ2kGp\":[\"Load more\"],\"y_0uwd\":[\"Yesterday\"],\"yxlcv5\":[\"Turn off the monthly recap?\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"z0ROB3\":[\"Remove like\"],\"z1uNN0\":[\"No emoji found.\"],\"zVuxvN\":[\"Refreshing…\"],\"zwBp5t\":[\"Private\"]}"); \ No newline at end of file +/*eslint-disable*/export const messages=JSON.parse("{\"-K9EZb\":[\"Add email…\"],\"-Ya-b9\":[\"Failed to save\"],\"-iDCtG\":[\"Your settings\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Search dumps, users, playlists…\"],\"1QfxQT\":[\"Dismiss\"],\"1YKhJ3\":[\"1 new message\"],\"1njn7W\":[\"Light\"],\"1utXA6\":[\"Dumps\"],\"26iNma\":[\"Post comment\"],\"2DoBvq\":[\"Feeds\"],\"2Hlmdt\":[\"Write a reply…\"],\"2_DHgA\":[\"URL slug\"],\"2nrsEn\":[\"deleted message\"],\"2wxgft\":[\"Rename\"],\"2ygf_L\":[\"← Back\"],\"3KKSM4\":[\"private\"],\"3QuNUr\":[\"Several people are typing…\"],\"3SEwY1\":[\"Delete category \\\"\",[\"0\"],\"\\\"? This cannot be undone.\"],\"3qkggm\":[\"Fullscreen\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" followed your playlist <1>\",[\"1\"],\"\"],\"48e2R8\":[\"Edit email\"],\"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…\"],\"5NZ3tg\":[\"Turning off…\"],\"5TviPn\":[\"Cancel search\"],\"5cC8f2\":[\"Edited \",[\"0\"]],\"5oD9f_\":[\"Earlier\"],\"6Qly-0\":[\"a comment\"],\"6YE0P6\":[\"Editing \",[\"0\"]],\"6YtxFj\":[\"Name\"],\"6_dCYd\":[\"Overview\"],\"6gRgw8\":[\"Retry\"],\"7PHCIN\":[\"Cancel removal\"],\"7PzzBU\":[\"User\"],\"7d1a0d\":[\"Public\"],\"7noMWp\":[\"Nothing happened this week.\"],\"7sNhEz\":[\"Username\"],\"8-8abB\":[\"Disconnected — reconnecting…\"],\"8F1i42\":[\"Page not found\"],\"8JplJJ\":[\"Unsubscribed\"],\"8Ug9jB\":[\"Related\"],\"8ZsakT\":[\"Password\"],\"92o1-U\":[\"Failed to delete\"],\"9BruTc\":[\"Why?\"],\"9l4qcT\":[\"Drop a replacement here\"],\"9uI_rE\":[\"Undo\"],\"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\"],\"AhNP40\":[\"Seek\"],\"AnRu_j\":[\"Cancel reply\"],\"B4LeYP\":[\"Delete failed (\",[\"0\"],\")\"],\"BjitBo\":[\"Download \",[\"0\"]],\"BlpJ2G\":[\"Picked up where you left off.\"],\"CI50ct\":[\"Upvoted\"],\"C_qQE4\":[\"Already dumped by \",[\"0\"],\" \",[\"1\"]],\"Cf_gxi\":[\"Turn off the weekly digest?\"],\"Cj24wt\":[\"Registering…\"],\"CnqayG\":[\"Couldn't update. Try again.\"],\"D8DRYV\":[\"Delete this message?\"],\"DI3NBO\":[\"Open on \",[\"0\"]],\"DPfwMq\":[\"Done\"],\"DdeHXH\":[\"Delete this dump? This cannot be undone.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" upvoted <1>\",[\"1\"],\"\"],\"E417RG\":[\"You won't get the weekly digest any more. You can turn it back on in your settings.\"],\"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.\"],\"EwVrWF\":[\"The dump couldn't be removed. Try again.\"],\"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\"],\"Gv5nA5\":[\"Unexpected response format\"],\"H4MGkI\":[\"Expand player\"],\"H4o4sk\":[\"No one yet.\"],\"H8pzW-\":[\"Failed to update avatar\"],\"HTLDA4\":[\"Loading more…\"],\"H_PnT8\":[\"Insert emoji\"],\"I-x669\":[\"Invitees\"],\"IF7RiR\":[\"Cover\"],\"IO9KL_\":[\"Next track\"],\"IZX7TO\":[\"Failed to generate invite\"],\"IagCbF\":[\"URL\"],\"IgrLD_\":[\"Pause\"],\"IhK90M\":[\"Letters, numbers and underscores only\"],\"ImOQa9\":[\"Reply\"],\"JJ-Bhk\":[\"Your email address\"],\"JLasL-\":[\"Couldn't unsubscribe\"],\"JRQitQ\":[\"Confirm new password\"],\"JXr41k\":[\"<0>\",[\"0\"],\" commented on <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…\"],\"MHrjPM\":[\"Title\"],\"MKEPCY\":[\"Follow\"],\"MTb2Ot\":[\"No messages yet. Say hello!\"],\"Mj6oCJ\":[[\"0\"],\" is typing…\"],\"N40H-G\":[\"All\"],\"N6j2JH\":[\"Edit \",[\"0\"]],\"NUrY9o\":[\"Categories\"],\"Nu4oKW\":[\"Description\"],\"O32mmL\":[\"Edit description\"],\"O3oNi5\":[\"Email\"],\"Oprv1v\":[\"Password (min. \",[\"0\"],\" characters)\"],\"P5MuiB\":[\"Add image\"],\"PJTy3N\":[[\"totalUnread\",\"plural\",{\"one\":[\"#\",\" new\"],\"other\":[\"#\",\" 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 files\"],\"QLtPBd\":[\"No dumps in this playlist yet.\"],\"QctOgc\":[\"Could not connect to the server. Check your connection and try again.\"],\"R7SsBE\":[\"Mute\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Delete this playlist? This cannot be undone.\"],\"RTksSy\":[\"<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…\"],\"SMaFdc\":[\"Unsubscribe\"],\"Sad2tK\":[\"Sending…\"],\"So7gCD\":[\"Paste a link…\"],\"StovX6\":[\"This page does not exist.\"],\"Sxm8rQ\":[\"Users\"],\"T8ejtZ\":[\"This comment couldn't be deleted. Try again.\"],\"T91vKp\":[\"Play\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" was added to <1>\",[\"1\"],\"\"],\"TAtbho\":[\"Loading messages…\"],\"TN382O\":[\"Invalid link\"],\"Tv9vbB\":[\"Follow playlist\"],\"Tz0i8g\":[\"Settings\"],\"U3pytU\":[\"Admin\"],\"UNMVei\":[\"Forgot password?\"],\"UOZith\":[\"Failed to post\"],\"URAieT\":[\"Log in to vote\"],\"V38P77\":[\"Only its poster or a moderator can edit this dump.\"],\"VCoEm-\":[\"Back to login\"],\"VKsaTi\":[\"Replying to\"],\"VLAd8f\":[\"Max \",[\"limit\"]],\"V_e7nf\":[\"Set new password\"],\"VnNJbN\":[\"From playlists\"],\"VskHIx\":[\"Delete category\"],\"VyTYmS\":[\"Change avatar\"],\"W5A0Ly\":[\"An unexpected error occurred.\"],\"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\"],\"XnL-Eu\":[\"No users match \\\"\",[\"q\"],\"\\\".\"],\"Xs2Lez\":[\"This reset link is missing or malformed.\"],\"YK1Dhc\":[\"a post\"],\"YWnSuq\":[\"This unsubscribe link is missing or malformed.\"],\"YZxXK8\":[\"Close emoji picker\"],\"YcjhhX\":[\"User management\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" liked your comment on <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…\"],\"_I1_HN\":[\"Lowercase letters, numbers and single hyphens only\"],\"_R_sGB\":[\"Password changed successfully.\"],\"_aept4\":[\"Post reply\"],\"_cF7Rs\":[\"Volume\"],\"_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…\"],\"b_1P1D\":[\"Renaming…\"],\"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\"],\"digest.community\":[\"Community\"],\"digest.count.comments\":[[\"n\",\"plural\",{\"one\":[\"comment\"],\"other\":[\"comments\"]}]],\"digest.count.upvotes\":[[\"n\",\"plural\",{\"one\":[\"upvote\"],\"other\":[\"upvotes\"]}]],\"digest.invitedBy\":[\"invited by @\",[\"username\"]],\"digest.invitedByYou\":[\"invited by you\"],\"digest.monthly\":[\"Monthly recap\"],\"digest.monthly.off\":[\"Turn off monthly recaps\"],\"digest.newMembers\":[\"New members\"],\"digest.newPlaylists\":[\"New public playlists\"],\"digest.stat.chat\":[[\"n\",\"plural\",{\"one\":[\"chat message\"],\"other\":[\"chat messages\"]}]],\"digest.stat.dumps\":[[\"n\",\"plural\",{\"one\":[\"dump\"],\"other\":[\"dumps\"]}]],\"digest.topMonth\":[\"Top this month\"],\"digest.topWeek\":[\"Top this week\"],\"digest.weekly\":[\"Weekly digest\"],\"digest.weekly.off\":[\"Turn off weekly digests\"],\"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.\"],\"email.digest.addedTo\":[\"added to \",[\"playlist\"]],\"email.digest.andMore\":[\"and \",[\"n\"],\" more\"],\"email.digest.by\":[\"by @\",[\"username\"]],\"email.digest.chatWeek\":[[\"n\",\"plural\",{\"one\":[\"#\",\" message this week\"],\"other\":[\"#\",\" messages this week\"]}]],\"email.digest.communityPage\":[\"See it all on the community page\"],\"email.digest.count.followers\":[[\"n\",\"plural\",{\"one\":[\"new follower\"],\"other\":[\"new followers\"]}]],\"email.digest.count.likes\":[[\"n\",\"plural\",{\"one\":[\"comment like\"],\"other\":[\"comment likes\"]}]],\"email.digest.count.mentions\":[[\"n\",\"plural\",{\"one\":[\"mention\"],\"other\":[\"mentions\"]}]],\"email.digest.count.playlistFollows\":[[\"n\",\"plural\",{\"one\":[\"playlist follow\"],\"other\":[\"playlist follows\"]}]],\"email.digest.forYou\":[\"For you\"],\"email.digest.hl.comment\":[[\"who\"],\" commented on your dump \",[\"target\"]],\"email.digest.hl.commentLike\":[[\"who\"],\" liked your comment on \",[\"target\"]],\"email.digest.hl.mentionComment\":[[\"who\"],\" mentioned you in a comment on \",[\"target\"]],\"email.digest.hl.mentionEveryone\":[[\"who\"],\" mentioned everyone in \",[\"target\"]],\"email.digest.hl.mentionIn\":[[\"who\"],\" mentioned you in \",[\"target\"]],\"email.digest.hl.mentionPlaylist\":[[\"who\"],\" mentioned you in the playlist \",[\"target\"]],\"email.digest.hl.playlistFollow\":[[\"who\"],\" followed your playlist \",[\"target\"]],\"email.digest.hl.userFollow\":[[\"who\"],\" started following you\"],\"email.digest.inChat\":[\"In the chat\"],\"email.digest.monthly.reason\":[\"You're getting the monthly recap because it's on in your settings.\"],\"email.digest.monthly.subject\":[\"The month on \",[\"site\"],\", \",[\"range\"]],\"email.digest.monthly.title\":[\"The month on \",[\"site\"]],\"email.digest.more\":[[\"n\"],\" more on your Followed feed\"],\"email.digest.network\":[\"From people you follow\"],\"email.digest.openChat\":[\"Open the chat\"],\"email.digest.playlistMeta\":[\"by @\",[\"username\"],\" · \",[\"n\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}]],\"email.digest.pre.members\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new member\"],\"other\":[\"#\",\" new members\"]}]],\"email.digest.pre.network\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new dump from people you follow\"],\"other\":[\"#\",\" new dumps from people you follow\"]}]],\"email.digest.pre.playlists\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new playlist\"],\"other\":[\"#\",\" new playlists\"]}]],\"email.digest.pre.topMonth\":[[\"n\",\"plural\",{\"one\":[\"the most upvoted dump of the month\"],\"other\":[\"the \",\"#\",\" most upvoted dumps of the month\"]}]],\"email.digest.pre.topWeek\":[[\"n\",\"plural\",{\"one\":[\"the most upvoted dump of the week\"],\"other\":[\"the \",\"#\",\" most upvoted dumps of the week\"]}]],\"email.digest.seeAll\":[[\"n\",\"plural\",{\"one\":[\"See the notification\"],\"other\":[\"See all \",\"#\",\" notifications\"]}]],\"email.digest.theChat\":[\"the chat\"],\"email.digest.totals\":[[\"dumps\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}],\", \",[\"upvotes\",\"plural\",{\"one\":[\"#\",\" upvote\"],\"other\":[\"#\",\" upvotes\"]}],\" and \",[\"comments\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}],\".\"],\"email.digest.upload\":[\"upload\"],\"email.digest.weekly.reason\":[\"You're getting the weekly digest because it's on in your settings.\"],\"email.digest.weekly.subject\":[\"Your week on \",[\"site\"],\", \",[\"range\"]],\"email.digest.weekly.title\":[\"Your week, \",[\"username\"]],\"email.digest.yours\":[\"yours\"],\"email.reset.body\":[\"Someone asked to reset the password of your \",[\"site\"],\" account. The button below lets you choose a new one. It works once, for \",[\"hours\",\"plural\",{\"one\":[\"#\",\" hour\"],\"other\":[\"#\",\" hours\"]}],\".\"],\"email.reset.button\":[\"Reset password\"],\"email.reset.fallback\":[\"If the button doesn't work, paste this link into your browser:\"],\"email.reset.hi\":[\"Hi \",[\"username\"],\",\"],\"email.reset.ignore\":[\"If you didn't ask for this, ignore this email. Your password stays the same.\"],\"email.reset.preheader\":[\"The link works once, for \",[\"hours\",\"plural\",{\"one\":[\"#\",\" hour\"],\"other\":[\"#\",\" hours\"]}],\".\"],\"email.reset.reason\":[\"You're getting this email because a password reset was requested for this address.\"],\"email.reset.subject\":[\"Reset your \",[\"site\"],\" password\"],\"email.reset.title\":[\"Reset your password\"],\"email.settings\":[\"Settings\"],\"email.welcome.body\":[\"# Welcome to \",[\"site\"],\"!\\n\\nHi **\",[\"username\"],\"**,\\n\\nYour account has been created successfully. Welcome aboard!\"],\"email.welcome.button\":[\"Open \",[\"site\"]],\"email.welcome.digests\":[\"Once a week you'll get a digest of what happened here, and once a month a recap of the most upvoted dumps. You can turn either off in your settings.\"],\"email.welcome.preheader\":[\"Your account is ready.\"],\"email.welcome.reason\":[\"You're getting this email because an account was created on \",[\"site\"],\" with this address.\"],\"email.welcome.subject\":[\"Welcome to \",[\"site\"]],\"en9o7K\":[\"Failed to post comment\"],\"etFQQS\":[\"What makes it worth it?\"],\"f3cEDO\":[\"Change image\"],\"fC6mXb\":[\"Upvote\"],\"fI-mNw\":[\"Playlists\"],\"fgLNSM\":[\"Register\"],\"gANddk\":[\"Uploading…\"],\"gGx5tM\":[\"Editing\"],\"gIQQwD\":[\"Failed to load\"],\"gIpLuy\":[\"A link is required\"],\"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\"],\" posted <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\"],\"jTGs8A\":[\"This site doesn't send email, so no digest goes out.\"],\"jbernk\":[\"Loading profile…\"],\"jnrIbz\":[\"Upvoted (\",[\"0\"],\")\"],\"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\"],\"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\"],\"mDJo8W\":[\"Dumps (\",[\"0\"],\")\"],\"mb_Wju\":[\"Drop a file here, or paste one\"],\"mt6O6E\":[\"This is a mirage.\"],\"nbm5sI\":[\"No dumps match \\\"\",[\"q\"],\"\\\".\"],\"nrjqON\":[\"Checking invite…\"],\"nucyoV\":[\"<0>\",[\"0\"],\" mentioned everyone in <1>\",[\"where\"],\"\"],\"nwtY4N\":[\"Something went wrong\"],\"nx4kaN\":[\"Could not save\"],\"oTYvvN\":[[\"0\",\"plural\",{\"one\":[\"and \",\"#\",\" earlier dump\"],\"other\":[\"and \",\"#\",\" earlier dumps\"]}]],\"oW3yYY\":[\"This month\"],\"ogtYkT\":[\"Password updated\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" mentioned you in <1>\",[\"where\"],\"\"],\"pSheLH\":[\"No invitees yet.\"],\"pd3FCq\":[\"That file is over \",[\"limit\"],\".\"],\"pvnfJD\":[\"Dark\"],\"qIMfNQ\":[\"Delete playlist\"],\"qItrqH\":[\"You won't get the monthly recap any more. You can turn it back on in your settings.\"],\"qbDAcy\":[\"Dump it\"],\"qgx_78\":[\"Follow some public playlists to see their dumps here.\"],\"qnUJBI\":[\"The new order couldn't be saved. Try again.\"],\"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\"],\"rn6SBY\":[\"Unmute\"],\"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\"],\"tyiiHe\":[\"Nothing happened this month.\"],\"u1lDX2\":[\"Fetching preview…\"],\"uD0qXQ\":[\"Drop a file here\"],\"uISnsY\":[\"Could not delete\"],\"uMGUnV\":[\"No playlists yet.\"],\"ub1EEL\":[\"edited \",[\"0\"]],\"uomLas\":[\"Add category\"],\"v-Wp--\":[\"Open source\"],\"vJBF1r\":[\"Posting…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" unread)\"],\"vQMkHu\":[\"Remove vote\"],\"vuosjb\":[\"User menu\"],\"wckWOP\":[\"Manage\"],\"wixIgH\":[\"Already have an account? <0>Log in\"],\"wtxFp4\":[\"File too large (max \",[\"max\"],\").\"],\"x6tjuK\":[\"Default tab\"],\"xEWkgZ\":[\"← Back to all dumps\"],\"xPHtx0\":[\"Submit search\"],\"xVuNgt\":[\"+ New playlist\"],\"xc9O_u\":[\"Delete dump\"],\"y1G_DI\":[\"Could not rename\"],\"y6sq5j\":[\"Following\"],\"yA_6BX\":[\"View all →\"],\"yBBtRm\":[\"Follow some users to see their dumps here.\"],\"yByRxz\":[\"This week\"],\"yQ2kGp\":[\"Load more\"],\"y_0uwd\":[\"Yesterday\"],\"yxlcv5\":[\"Turn off the monthly recap?\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"z0ROB3\":[\"Remove like\"],\"z1uNN0\":[\"No emoji found.\"],\"zVuxvN\":[\"Refreshing…\"],\"zwBp5t\":[\"Private\"]}"); \ No newline at end of file diff --git a/locales/en.po b/locales/en.po index 9dc78a52..027a8f85 100644 --- a/locales/en.po +++ b/locales/en.po @@ -14,175 +14,180 @@ msgstr "" "Plural-Forms: \n" #. js-lingui-explicit-id -#: api/mail/digest.ts:129 +#: api/mail/digest.ts:163 +msgid "email.digest.hl.mentionEveryone" +msgstr "{who} mentioned everyone in {target}" + +#. js-lingui-explicit-id +#: api/mail/digest.ts:130 #: src/pages/Community.tsx:49 msgid "digest.count.upvotes" msgstr "{n, plural, one {upvote} other {upvotes}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:131 +#: api/mail/digest.ts:132 msgid "email.digest.count.followers" msgstr "{n, plural, one {new follower} other {new followers}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:133 +#: api/mail/digest.ts:134 #: src/pages/Community.tsx:50 msgid "digest.count.comments" msgstr "{n, plural, one {comment} other {comments}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:135 +#: api/mail/digest.ts:136 msgid "email.digest.count.mentions" msgstr "{n, plural, one {mention} other {mentions}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:137 +#: api/mail/digest.ts:138 msgid "email.digest.count.likes" msgstr "{n, plural, one {comment like} other {comment likes}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:139 +#: api/mail/digest.ts:140 msgid "email.digest.count.playlistFollows" msgstr "{n, plural, one {playlist follow} other {playlist follows}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:155 +#: api/mail/digest.ts:156 msgid "email.digest.hl.mentionComment" msgstr "{who} mentioned you in a comment on {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:158 +#: api/mail/digest.ts:159 msgid "email.digest.hl.mentionIn" msgstr "{who} mentioned you in {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:160 +#: api/mail/digest.ts:161 msgid "email.digest.hl.mentionPlaylist" msgstr "{who} mentioned you in the playlist {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:162 +#: api/mail/digest.ts:165 msgid "email.digest.hl.comment" msgstr "{who} commented on your dump {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:164 +#: api/mail/digest.ts:167 msgid "email.digest.hl.playlistFollow" msgstr "{who} followed your playlist {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:166 +#: api/mail/digest.ts:169 msgid "email.digest.hl.userFollow" msgstr "{who} started following you" #. js-lingui-explicit-id -#: api/mail/digest.ts:168 +#: api/mail/digest.ts:171 msgid "email.digest.hl.commentLike" msgstr "{who} liked your comment on {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:172 +#: api/mail/digest.ts:175 msgid "email.digest.by" msgstr "by @{username}" #. js-lingui-explicit-id -#: api/mail/digest.ts:174 +#: api/mail/digest.ts:177 msgid "email.digest.addedTo" msgstr "added to {playlist}" #. js-lingui-explicit-id -#: api/mail/digest.ts:175 +#: api/mail/digest.ts:178 msgid "email.digest.upload" msgstr "upload" #. js-lingui-explicit-id -#: api/mail/digest.ts:178 +#: api/mail/digest.ts:181 #: src/pages/Community.tsx:141 msgid "digest.invitedByYou" msgstr "invited by you" #. js-lingui-explicit-id -#: api/mail/digest.ts:179 +#: api/mail/digest.ts:182 #: src/pages/Community.tsx:143 msgid "digest.invitedBy" msgstr "invited by @{username}" #. js-lingui-explicit-id -#: api/mail/digest.ts:180 +#: api/mail/digest.ts:183 msgid "email.digest.andMore" msgstr "and {n} more" #. js-lingui-explicit-id -#: api/mail/digest.ts:182 +#: api/mail/digest.ts:185 msgid "email.digest.playlistMeta" msgstr "by @{username} · {n, plural, one {# dump} other {# dumps}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:205 +#: api/mail/digest.ts:208 msgid "email.digest.yours" msgstr "yours" #. js-lingui-explicit-id -#: api/mail/digest.ts:223 +#: api/mail/digest.ts:226 #: src/pages/Settings.tsx:154 msgid "digest.monthly" msgstr "Monthly recap" #. js-lingui-explicit-id -#: api/mail/digest.ts:223 +#: api/mail/digest.ts:226 #: src/pages/Settings.tsx:145 msgid "digest.weekly" msgstr "Weekly digest" #. js-lingui-explicit-id -#: api/mail/digest.ts:231 +#: api/mail/digest.ts:234 msgid "email.digest.monthly.title" msgstr "The month on {site}" #. js-lingui-explicit-id -#: api/mail/digest.ts:232 +#: api/mail/digest.ts:235 msgid "email.digest.weekly.title" msgstr "Your week, {username}" #. js-lingui-explicit-id -#: api/mail/digest.ts:240 +#: api/mail/digest.ts:243 msgid "email.digest.forYou" msgstr "For you" #. js-lingui-explicit-id -#: api/mail/digest.ts:249 +#: api/mail/digest.ts:252 msgid "email.digest.theChat" msgstr "the chat" #. js-lingui-explicit-id -#: api/mail/digest.ts:264 +#: api/mail/digest.ts:267 msgid "email.digest.seeAll" msgstr "{n, plural, one {See the notification} other {See all # notifications}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:273 +#: api/mail/digest.ts:276 msgid "email.digest.network" msgstr "From people you follow" #. js-lingui-explicit-id -#: api/mail/digest.ts:279 +#: api/mail/digest.ts:282 msgid "email.digest.more" msgstr "{n} more on your Followed feed" #. js-lingui-explicit-id -#: api/mail/digest.ts:291 +#: api/mail/digest.ts:294 #: src/pages/Community.tsx:127 msgid "digest.topMonth" msgstr "Top this month" #. js-lingui-explicit-id -#: api/mail/digest.ts:291 +#: api/mail/digest.ts:294 #: src/pages/Community.tsx:126 msgid "digest.topWeek" msgstr "Top this week" #. js-lingui-explicit-id -#: api/mail/digest.ts:307 +#: api/mail/digest.ts:310 #: src/components/layout/UserMenu.tsx:94 #: src/pages/Community.tsx:73 #: src/pages/Community.tsx:100 @@ -190,113 +195,113 @@ msgid "digest.community" msgstr "Community" #. js-lingui-explicit-id -#: api/mail/digest.ts:314 +#: api/mail/digest.ts:317 #: src/pages/Community.tsx:48 msgid "digest.stat.dumps" msgstr "{n, plural, one {dump} other {dumps}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:320 +#: api/mail/digest.ts:323 #: src/pages/Community.tsx:53 msgid "digest.stat.chat" msgstr "{n, plural, one {chat message} other {chat messages}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:329 +#: api/mail/digest.ts:332 #: src/pages/Community.tsx:133 msgid "digest.newMembers" msgstr "New members" #. js-lingui-explicit-id -#: api/mail/digest.ts:348 +#: api/mail/digest.ts:351 #: src/pages/Community.tsx:154 msgid "digest.newPlaylists" msgstr "New public playlists" #. js-lingui-explicit-id -#: api/mail/digest.ts:363 +#: api/mail/digest.ts:366 msgid "email.digest.inChat" msgstr "In the chat" #. js-lingui-explicit-id -#: api/mail/digest.ts:365 +#: api/mail/digest.ts:368 msgid "email.digest.chatWeek" msgstr "{n, plural, one {# message this week} other {# messages this week}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:368 +#: api/mail/digest.ts:371 msgid "email.digest.openChat" msgstr "Open the chat" #. js-lingui-explicit-id -#: api/mail/digest.ts:377 +#: api/mail/digest.ts:380 msgid "email.digest.totals" msgstr "{dumps, plural, one {# dump} other {# dumps}}, {upvotes, plural, one {# upvote} other {# upvotes}} and {comments, plural, one {# comment} other {# comments}}." #. js-lingui-explicit-id -#: api/mail/digest.ts:383 +#: api/mail/digest.ts:386 msgid "email.digest.communityPage" msgstr "See it all on the community page" #. js-lingui-explicit-id -#: api/mail/digest.ts:393 +#: api/mail/digest.ts:396 msgid "email.digest.monthly.reason" msgstr "You're getting the monthly recap because it's on in your settings." #. js-lingui-explicit-id -#: api/mail/digest.ts:394 +#: api/mail/digest.ts:397 msgid "email.digest.weekly.reason" msgstr "You're getting the weekly digest because it's on in your settings." #. js-lingui-explicit-id -#: api/mail/digest.ts:398 +#: api/mail/digest.ts:401 #: src/pages/Unsubscribe.tsx:118 msgid "digest.monthly.off" msgstr "Turn off monthly recaps" #. js-lingui-explicit-id -#: api/mail/digest.ts:398 +#: api/mail/digest.ts:401 #: src/pages/Unsubscribe.tsx:117 msgid "digest.weekly.off" msgstr "Turn off weekly digests" #. js-lingui-explicit-id -#: api/mail/digest.ts:403 +#: api/mail/digest.ts:406 msgid "email.settings" msgstr "Settings" #. js-lingui-explicit-id -#: api/mail/digest.ts:411 +#: api/mail/digest.ts:414 msgid "email.digest.monthly.subject" msgstr "The month on {site}, {range}" #. js-lingui-explicit-id -#: api/mail/digest.ts:415 +#: api/mail/digest.ts:418 msgid "email.digest.weekly.subject" msgstr "Your week on {site}, {range}" #. js-lingui-explicit-id -#: api/mail/digest.ts:438 +#: api/mail/digest.ts:441 msgid "email.digest.pre.network" msgstr "{n, plural, one {# new dump from people you follow} other {# new dumps from people you follow}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:447 +#: api/mail/digest.ts:450 msgid "email.digest.pre.topMonth" msgstr "{n, plural, one {the most upvoted dump of the month} other {the # most upvoted dumps of the month}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:451 +#: api/mail/digest.ts:454 msgid "email.digest.pre.members" msgstr "{n, plural, one {# new member} other {# new members}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:454 +#: api/mail/digest.ts:457 msgid "email.digest.pre.playlists" msgstr "{n, plural, one {# new playlist} other {# new playlists}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:458 +#: api/mail/digest.ts:461 msgid "email.digest.pre.topWeek" msgstr "{n, plural, one {the most upvoted dump of the week} other {the # most upvoted dumps of the week}}" @@ -401,12 +406,12 @@ msgstr "{0, plural, one {and # earlier dump} other {and # earlier dumps}}" #. placeholder {0}: typingNames[0] #. placeholder {1}: typingNames[1] -#: src/components/chat/ChatModal.tsx:649 +#: src/components/chat/ChatModal.tsx:650 msgid "{0} and {1} are typing…" msgstr "{0} and {1} are typing…" #. placeholder {0}: typingNames[0] -#: src/components/chat/ChatModal.tsx:647 +#: src/components/chat/ChatModal.tsx:648 msgid "{0} is typing…" msgstr "{0} is typing…" @@ -425,11 +430,11 @@ msgstr "{0}–{1} characters: letters, numbers, or underscores" msgid "{label} ({count})" msgstr "{label} ({count})" -#: src/pages/Notifications.tsx:499 +#: src/pages/Notifications.tsx:508 msgid "{totalUnread, plural, one {# new} other {# new}}" msgstr "{totalUnread, plural, one {# new} other {# new}}" -#: src/components/chat/ChatModal.tsx:778 +#: src/components/chat/ChatModal.tsx:779 msgid "{unseenCount} new messages" msgstr "{unseenCount} new messages" @@ -480,7 +485,12 @@ msgid "<0>{0} liked your comment on <1>{1}" msgstr "<0>{0} liked your comment on <1>{1}" #. placeholder {0}: d.mentionerUsername -#: src/pages/Notifications.tsx:265 +#: src/pages/Notifications.tsx:266 +msgid "<0>{0} mentioned everyone in <1>{where}" +msgstr "<0>{0} mentioned everyone in <1>{where}" + +#. placeholder {0}: d.mentionerUsername +#: src/pages/Notifications.tsx:274 msgid "<0>{0} mentioned you in <1>{where}" msgstr "<0>{0} mentioned you in <1>{where}" @@ -507,7 +517,7 @@ msgstr "<0>{0} upvoted <1>{1}" msgid "<0>{0} was added to <1>{1}" msgstr "<0>{0} was added to <1>{1}" -#: src/components/chat/ChatModal.tsx:777 +#: src/components/chat/ChatModal.tsx:778 msgid "1 new message" msgstr "1 new message" @@ -597,7 +607,7 @@ msgid "Back to login" msgstr "Back to login" #: src/contexts/ws/WSProvider.tsx:395 -#: src/contexts/ws/WSProvider.tsx:761 +#: src/contexts/ws/WSProvider.tsx:764 msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects." msgstr "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects." @@ -620,8 +630,8 @@ msgstr "Cancel" msgid "Cancel removal" msgstr "Cancel removal" -#: src/components/chat/ChatModal.tsx:838 #: src/components/chat/ChatModal.tsx:839 +#: src/components/chat/ChatModal.tsx:840 msgid "Cancel reply" msgstr "Cancel reply" @@ -659,7 +669,7 @@ msgstr "Change password…" msgid "Change skin tone" msgstr "Change skin tone" -#: src/components/chat/ChatModal.tsx:738 +#: src/components/chat/ChatModal.tsx:739 #: src/components/layout/ChatButton.tsx:30 #: src/components/layout/ChatFab.tsx:54 #: src/components/layout/ChatFab.tsx:55 @@ -709,7 +719,7 @@ msgstr "Color scheme" msgid "Confirm new password" msgstr "Confirm new password" -#: src/contexts/ws/WSProvider.tsx:754 +#: src/contexts/ws/WSProvider.tsx:757 msgid "Connection lost — this message may not have been sent" msgstr "Connection lost — this message may not have been sent" @@ -746,7 +756,7 @@ 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/chat/ChatModal.tsx:664 +#: src/components/chat/ChatModal.tsx:665 msgid "Couldn't load the chat." msgstr "Couldn't load the chat." @@ -788,9 +798,9 @@ msgstr "Default tab" #: src/components/category/CategoryManager.tsx:236 #: src/components/category/CategoryManager.tsx:237 -#: src/components/chat/ChatMessageItem.tsx:115 -#: src/components/chat/ChatMessageItem.tsx:116 -#: src/components/chat/ChatMessageItem.tsx:214 +#: src/components/chat/ChatMessageItem.tsx:118 +#: src/components/chat/ChatMessageItem.tsx:119 +#: src/components/chat/ChatMessageItem.tsx:220 #: src/components/comment/CommentNode.tsx:265 #: src/components/comment/CommentNode.tsx:271 #: src/components/form/DeleteButton.tsx:36 @@ -832,7 +842,7 @@ msgstr "Delete this comment?" msgid "Delete this dump? This cannot be undone." msgstr "Delete this dump? This cannot be undone." -#: src/components/chat/ChatMessageItem.tsx:213 +#: src/components/chat/ChatMessageItem.tsx:219 msgid "Delete this message?" msgstr "Delete this message?" @@ -857,12 +867,12 @@ msgstr "Description (optional)" msgid "Discard" msgstr "Discard" -#: src/components/chat/ChatModal.tsx:742 +#: src/components/chat/ChatModal.tsx:743 msgid "Disconnected — reconnecting…" msgstr "Disconnected — reconnecting…" -#: src/components/chat/ChatModal.tsx:814 #: src/components/chat/ChatModal.tsx:815 +#: src/components/chat/ChatModal.tsx:816 msgid "Dismiss" msgstr "Dismiss" @@ -910,12 +920,12 @@ msgstr "Dumps" msgid "Dumps ({0})" msgstr "Dumps ({0})" -#: src/pages/Notifications.tsx:305 +#: src/pages/Notifications.tsx:314 msgid "Earlier" msgstr "Earlier" -#: src/components/chat/ChatMessageItem.tsx:104 -#: src/components/chat/ChatMessageItem.tsx:105 +#: src/components/chat/ChatMessageItem.tsx:107 +#: src/components/chat/ChatMessageItem.tsx:108 #: src/components/chat/PendingItem.tsx:93 #: src/components/comment/CommentNode.tsx:256 #: src/components/playlist/PlaylistHeader.tsx:59 @@ -941,14 +951,14 @@ msgstr "Edit email" msgid "Edit title" msgstr "Edit title" -#: src/components/chat/ChatMessageItem.tsx:128 +#: src/components/chat/ChatMessageItem.tsx:131 msgid "edited" msgstr "edited" #. placeholder {0}: relativeTime(comment.updatedAt) #. placeholder {0}: relativeTime(dump.updatedAt) #. placeholder {0}: relativeTime(message.updatedAt) -#: src/components/chat/ChatMessageItem.tsx:129 +#: src/components/chat/ChatMessageItem.tsx:132 #: src/components/comment/CommentNode.tsx:205 #: src/components/playlist/PlaylistHeader.tsx:94 #: src/pages/Dump.tsx:505 @@ -958,7 +968,7 @@ msgstr "edited {0}" #. placeholder {0}: comment.updatedAt.toLocaleString() #. placeholder {0}: dump.updatedAt.toLocaleString() #. placeholder {0}: message.updatedAt.toLocaleString() -#: src/components/chat/ChatMessageItem.tsx:125 +#: src/components/chat/ChatMessageItem.tsx:128 #: src/components/comment/CommentNode.tsx:203 #: src/components/playlist/PlaylistHeader.tsx:92 #: src/pages/Dump.tsx:503 @@ -1132,8 +1142,8 @@ msgstr "image" msgid "In collections" msgstr "In collections" -#: src/components/editor/TextEditor.tsx:573 -#: src/components/editor/TextEditor.tsx:575 +#: src/components/editor/TextEditor.tsx:579 +#: src/components/editor/TextEditor.tsx:581 msgid "Insert emoji" msgstr "Insert emoji" @@ -1190,7 +1200,7 @@ msgstr "Link" msgid "Link or file" msgstr "Link or file" -#: src/contexts/ws/WSProvider.tsx:760 +#: src/contexts/ws/WSProvider.tsx:763 msgid "Live updates are temporarily disconnected. Trying to reconnect…" msgstr "Live updates are temporarily disconnected. Trying to reconnect…" @@ -1202,7 +1212,7 @@ msgstr "Live updates unavailable." msgid "Load more" msgstr "Load more" -#: src/components/chat/ChatModal.tsx:763 +#: src/components/chat/ChatModal.tsx:764 msgid "Load older messages" msgstr "Load older messages" @@ -1211,7 +1221,7 @@ msgstr "Load older messages" msgid "Loading dump…" msgstr "Loading dump…" -#: src/components/chat/ChatModal.tsx:656 +#: src/components/chat/ChatModal.tsx:657 msgid "Loading messages…" msgstr "Loading messages…" @@ -1228,7 +1238,7 @@ msgid "Loading profile…" msgstr "Loading profile…" #: src/components/category/CategoryManager.tsx:60 -#: src/components/chat/ChatModal.tsx:762 +#: src/components/chat/ChatModal.tsx:763 #: src/components/editor/EmojiPickerPopup.tsx:125 #: src/components/playlist/PlaylistMembershipPanel.tsx:45 #: src/components/shared/ListState.tsx:38 @@ -1293,7 +1303,7 @@ msgstr "Mute" msgid "Name" msgstr "Name" -#: src/components/chat/ChatModal.tsx:691 +#: src/components/chat/ChatModal.tsx:692 #: src/components/feed/FeedTabBar.tsx:40 #: src/pages/Settings.tsx:52 msgid "New" @@ -1349,7 +1359,7 @@ msgstr "No followed playlists yet." msgid "No invitees yet." msgstr "No invitees yet." -#: src/components/chat/ChatModal.tsx:675 +#: src/components/chat/ChatModal.tsx:676 msgid "No messages yet. Say hello!" msgstr "No messages yet. Say hello!" @@ -1366,7 +1376,7 @@ msgstr "No playlists match \"{q}\"." msgid "No playlists yet." msgstr "No playlists yet." -#: src/contexts/ws/WSProvider.tsx:929 +#: src/contexts/ws/WSProvider.tsx:932 msgid "No reply from the server — this message may not have been sent" msgstr "No reply from the server — this message may not have been sent" @@ -1388,13 +1398,13 @@ msgstr "Nothing happened this week." #: src/components/profile/ProfileDumpsTab.tsx:53 #: src/components/profile/ProfileOverviewTab.tsx:47 -#: src/pages/Notifications.tsx:513 +#: src/pages/Notifications.tsx:522 msgid "Nothing here yet." msgstr "Nothing here yet." #: src/components/layout/NotificationBell.tsx:57 -#: src/pages/Notifications.tsx:378 -#: src/pages/Notifications.tsx:496 +#: src/pages/Notifications.tsx:387 +#: src/pages/Notifications.tsx:505 #: src/pages/Settings.tsx:128 msgid "Notifications" msgstr "Notifications" @@ -1411,7 +1421,7 @@ msgstr "Off" msgid "On" msgstr "On" -#: src/components/editor/TextEditor.tsx:385 +#: src/components/editor/TextEditor.tsx:391 msgid "Only images can be dropped here." msgstr "Only images can be dropped here." @@ -1594,13 +1604,13 @@ msgstr "Renaming…" msgid "Replace file" msgstr "Replace file" -#: src/components/chat/ChatMessageItem.tsx:94 -#: src/components/chat/ChatMessageItem.tsx:95 +#: src/components/chat/ChatMessageItem.tsx:97 +#: src/components/chat/ChatMessageItem.tsx:98 #: src/components/comment/CommentNode.tsx:244 msgid "Reply" msgstr "Reply" -#: src/components/chat/ChatModal.tsx:826 +#: src/components/chat/ChatModal.tsx:827 msgid "Replying to" msgstr "Replying to" @@ -1687,7 +1697,7 @@ msgstr "Searching…" msgid "Seek" msgstr "Seek" -#: src/components/chat/ChatModal.tsx:866 +#: src/components/chat/ChatModal.tsx:868 msgid "Send" msgstr "Send" @@ -1715,7 +1725,7 @@ msgstr "Set new password" msgid "Settings" msgstr "Settings" -#: src/components/chat/ChatModal.tsx:650 +#: src/components/chat/ChatModal.tsx:651 msgid "Several people are typing…" msgstr "Several people are typing…" @@ -1727,13 +1737,13 @@ msgstr "Single use, valid for 7 days." msgid "slug" msgstr "slug" -#: src/components/editor/TextEditor.tsx:343 +#: src/components/editor/TextEditor.tsx:349 msgid "Some images couldn't be uploaded." msgstr "Some images couldn't be uploaded." #: src/components/form/FormError.tsx:31 #: src/components/layout/PageError.tsx:29 -#: src/contexts/ws/WSProvider.tsx:729 +#: src/contexts/ws/WSProvider.tsx:732 #: src/pages/PlaylistDetail.tsx:124 msgid "Something went wrong" msgstr "Something went wrong" @@ -1766,7 +1776,7 @@ msgstr "the chat" msgid "The dump couldn't be removed. Try again." msgstr "The dump couldn't be removed. Try again." -#: src/components/editor/TextEditor.tsx:344 +#: src/components/editor/TextEditor.tsx:350 msgid "The image couldn't be uploaded." msgstr "The image couldn't be uploaded." @@ -1825,7 +1835,7 @@ msgstr "Title" msgid "Title is required." msgstr "Title is required." -#: src/pages/Notifications.tsx:303 +#: src/pages/Notifications.tsx:312 #: src/utils/chat/time.ts:24 msgid "Today" msgstr "Today" @@ -1835,7 +1845,7 @@ msgstr "Today" msgid "Track {0}" msgstr "Track {0}" -#: src/components/chat/ChatModal.tsx:667 +#: src/components/chat/ChatModal.tsx:668 msgid "Try again" msgstr "Try again" @@ -1851,7 +1861,7 @@ msgstr "Turn off the weekly digest?" msgid "Turning off…" msgstr "Turning off…" -#: src/components/chat/ChatModal.tsx:851 +#: src/components/chat/ChatModal.tsx:852 msgid "Type a message…" msgstr "Type a message…" @@ -1892,7 +1902,7 @@ msgstr "Upload failed" msgid "Uploading {0}%" msgstr "Uploading {0}%" -#: src/components/editor/TextEditor.tsx:595 +#: src/components/editor/TextEditor.tsx:601 msgid "Uploading image…" msgstr "Uploading image…" @@ -1990,7 +2000,7 @@ msgstr "Why?" msgid "Write a reply…" msgstr "Write a reply…" -#: src/pages/Notifications.tsx:304 +#: src/pages/Notifications.tsx:313 #: src/utils/chat/time.ts:27 msgid "Yesterday" msgstr "Yesterday" @@ -2007,11 +2017,11 @@ msgstr "You won't get the monthly recap any more. You can turn it back on in you msgid "You won't get the weekly digest any more. You can turn it back on in your settings." msgstr "You won't get the weekly digest any more. You can turn it back on in your settings." -#: src/pages/Notifications.tsx:516 +#: src/pages/Notifications.tsx:525 msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." msgstr "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." -#: src/contexts/ws/WSProvider.tsx:910 +#: src/contexts/ws/WSProvider.tsx:913 msgid "You're offline — not sent" msgstr "You're offline — not sent" diff --git a/locales/fr.mjs b/locales/fr.mjs index ab7ea22e..c53e36ec 100644 --- a/locales/fr.mjs +++ b/locales/fr.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"-K9EZb\":[\"Ajouter un e-mail…\"],\"-Ya-b9\":[\"Enregistrement échoué\"],\"-iDCtG\":[\"Vos paramètres\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Rechercher des recos, utilisateurs, collections…\"],\"1QfxQT\":[\"Fermer\"],\"1YKhJ3\":[\"1 nouveau message\"],\"1njn7W\":[\"Clair\"],\"1utXA6\":[\"Recos\"],\"26iNma\":[\"Publier le commentaire\"],\"2DoBvq\":[\"Flux\"],\"2Hlmdt\":[\"Écrire une réponse…\"],\"2_DHgA\":[\"Identifiant d’URL\"],\"2nrsEn\":[\"message supprimé\"],\"2wxgft\":[\"Renommer\"],\"2ygf_L\":[\"← Retour\"],\"3KKSM4\":[\"privé\"],\"3QuNUr\":[\"Plusieurs personnes écrivent…\"],\"3SEwY1\":[\"Supprimer la catégorie \\\"\",[\"0\"],\"\\\" ? Cette action est irréversible.\"],\"3qkggm\":[\"Plein écran\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" a suivi votre collection <1>\",[\"1\"],\"\"],\"48e2R8\":[\"Modifier l'e-mail\"],\"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…\"],\"5NZ3tg\":[\"Désactivation…\"],\"5TviPn\":[\"Annuler la recherche\"],\"5cC8f2\":[\"Modifié le \",[\"0\"]],\"5oD9f_\":[\"Plus tôt\"],\"6Qly-0\":[\"un commentaire\"],\"6YE0P6\":[\"Édition de \",[\"0\"]],\"6YtxFj\":[\"Nom\"],\"6_dCYd\":[\"Aperçu\"],\"6gRgw8\":[\"Réessayer\"],\"7PHCIN\":[\"Annuler la suppression\"],\"7PzzBU\":[\"Utilisateur\"],\"7d1a0d\":[\"Public\"],\"7noMWp\":[\"Rien de neuf cette semaine.\"],\"7sNhEz\":[\"Nom d'utilisateur\"],\"8-8abB\":[\"Déconnecté — reconnexion…\"],\"8F1i42\":[\"Page introuvable\"],\"8JplJJ\":[\"Désabonné\"],\"8Ug9jB\":[\"Connexe\"],\"8ZsakT\":[\"Mot de passe\"],\"92o1-U\":[\"Suppression échouée\"],\"9BruTc\":[\"Pourquoi ?\"],\"9l4qcT\":[\"Déposez un fichier de remplacement ici\"],\"9uI_rE\":[\"Annuler\"],\"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\"],\"AhNP40\":[\"Position\"],\"AnRu_j\":[\"Annuler la réponse\"],\"B4LeYP\":[\"Échec de la suppression (\",[\"0\"],\")\"],\"BjitBo\":[\"Télécharger \",[\"0\"]],\"BlpJ2G\":[\"Reprise de votre brouillon.\"],\"CI50ct\":[\"Voté\"],\"C_qQE4\":[\"Déjà recommandé par \",[\"0\"],\" \",[\"1\"]],\"Cf_gxi\":[\"Ne plus recevoir le résumé hebdomadaire ?\"],\"Cj24wt\":[\"Inscription…\"],\"CnqayG\":[\"La mise à jour a échoué. Réessayez.\"],\"D8DRYV\":[\"Supprimer ce message ?\"],\"DI3NBO\":[\"Ouvrir sur \",[\"0\"]],\"DPfwMq\":[\"Terminé\"],\"DdeHXH\":[\"Supprimer cette reco ? Cette action est irréversible.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" a voté pour <1>\",[\"1\"],\"\"],\"E417RG\":[\"Vous ne recevrez plus le résumé hebdomadaire. Vous pouvez le réactiver dans vos paramètres.\"],\"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é.\"],\"EwVrWF\":[\"Le dump n'a pas pu être retiré. Réessayez.\"],\"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\"],\"Gv5nA5\":[\"Format de réponse inattendu\"],\"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\"],\"IF7RiR\":[\"Couverture\"],\"IO9KL_\":[\"Piste suivante\"],\"IZX7TO\":[\"Impossible de générer une invitation\"],\"IagCbF\":[\"URL\"],\"IgrLD_\":[\"Pause\"],\"IhK90M\":[\"Lettres, chiffres et tirets bas seulement\"],\"ImOQa9\":[\"Répondre\"],\"JJ-Bhk\":[\"Votre adresse e-mail\"],\"JLasL-\":[\"Désabonnement impossible\"],\"JRQitQ\":[\"Confirmer le nouveau mot de passe\"],\"JXr41k\":[\"<0>\",[\"0\"],\" a commenté sur <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…\"],\"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\"],\"Nu4oKW\":[\"Description\"],\"O32mmL\":[\"Modifier la description\"],\"O3oNi5\":[\"E-mail\"],\"Oprv1v\":[\"Mot de passe (min. \",[\"0\"],\" caractères)\"],\"P5MuiB\":[\"Ajouter une image\"],\"PJTy3N\":[[\"totalUnread\",\"plural\",{\"one\":[\"#\",\" nouvelle\"],\"other\":[\"#\",\" nouvelles\"]}]],\"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 fichiers\"],\"QLtPBd\":[\"Aucune reco dans cette collection pour l'instant.\"],\"QctOgc\":[\"Impossible de joindre le serveur. Vérifiez votre connexion et réessayez.\"],\"R7SsBE\":[\"Couper le son\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Supprimer cette collection ? Cette action est irréversible.\"],\"RTksSy\":[\"<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…\"],\"SMaFdc\":[\"Se désabonner\"],\"Sad2tK\":[\"Envoi…\"],\"So7gCD\":[\"Collez un lien…\"],\"StovX6\":[\"Rien à voir, circulez.\"],\"Sxm8rQ\":[\"Utilisateurs\"],\"T8ejtZ\":[\"Ce commentaire n'a pas pu être supprimé. Réessayez.\"],\"T91vKp\":[\"Lecture\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" a été ajouté à <1>\",[\"1\"],\"\"],\"TAtbho\":[\"Chargement des messages…\"],\"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\"],\"V38P77\":[\"Seuls son auteur et les modérateurs peuvent modifier ce dump.\"],\"VCoEm-\":[\"Retour à la connexion\"],\"VKsaTi\":[\"En réponse à\"],\"VLAd8f\":[\"Max \",[\"limit\"]],\"V_e7nf\":[\"Définir un nouveau mot de passe\"],\"VnNJbN\":[\"De collections\"],\"VskHIx\":[\"Supprimer la catégorie\"],\"VyTYmS\":[\"Changer l'avatar\"],\"W5A0Ly\":[\"Une erreur inattendue s'est produite.\"],\"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\"],\"XnL-Eu\":[\"Aucun utilisateur ne correspond à « \",[\"q\"],\" ».\"],\"Xs2Lez\":[\"Ce lien de réinitialisation est absent ou malformé.\"],\"YK1Dhc\":[\"une publication\"],\"YWnSuq\":[\"Ce lien de désabonnement est absent ou mal formé.\"],\"YZxXK8\":[\"Fermer le sélecteur d'emoji\"],\"YcjhhX\":[\"Gestion utilisateur\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" a aimé votre commentaire sur <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…\"],\"_I1_HN\":[\"Minuscules, chiffres et tirets simples seulement\"],\"_R_sGB\":[\"Mot de passe modifié avec succès.\"],\"_aept4\":[\"Publier la réponse\"],\"_cF7Rs\":[\"Volume\"],\"_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…\"],\"b_1P1D\":[\"Renommage…\"],\"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\"],\"digest.community\":[\"Communauté\"],\"digest.count.comments\":[[\"n\",\"plural\",{\"one\":[\"commentaire\"],\"other\":[\"commentaires\"]}]],\"digest.count.upvotes\":[[\"n\",\"plural\",{\"one\":[\"vote\"],\"other\":[\"votes\"]}]],\"digest.invitedBy\":[\"sur invitation de @\",[\"username\"]],\"digest.invitedByYou\":[\"sur votre invitation\"],\"digest.monthly\":[\"Récap mensuel\"],\"digest.monthly.off\":[\"Ne plus recevoir le récap mensuel\"],\"digest.newMembers\":[\"Nouveaux membres\"],\"digest.newPlaylists\":[\"Nouvelles collections publiques\"],\"digest.stat.chat\":[[\"n\",\"plural\",{\"one\":[\"message dans la tribune\"],\"other\":[\"messages dans la tribune\"]}]],\"digest.stat.dumps\":[[\"n\",\"plural\",{\"one\":[\"reco\"],\"other\":[\"recos\"]}]],\"digest.topMonth\":[\"Les plus votées du mois\"],\"digest.topWeek\":[\"Les plus votées de la semaine\"],\"digest.weekly\":[\"Résumé hebdomadaire\"],\"digest.weekly.off\":[\"Ne plus recevoir le résumé hebdomadaire\"],\"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.\"],\"email.digest.addedTo\":[\"ajoutée à \",[\"playlist\"]],\"email.digest.andMore\":[\"et \",[\"n\"],\" de plus\"],\"email.digest.by\":[\"par @\",[\"username\"]],\"email.digest.chatWeek\":[[\"n\",\"plural\",{\"one\":[\"#\",\" message cette semaine\"],\"other\":[\"#\",\" messages cette semaine\"]}]],\"email.digest.communityPage\":[\"Tout voir sur la page de la communauté\"],\"email.digest.count.followers\":[[\"n\",\"plural\",{\"one\":[\"nouvel abonné\"],\"other\":[\"nouveaux abonnés\"]}]],\"email.digest.count.likes\":[[\"n\",\"plural\",{\"one\":[\"j'aime sur vos commentaires\"],\"other\":[\"j'aime sur vos commentaires\"]}]],\"email.digest.count.mentions\":[[\"n\",\"plural\",{\"one\":[\"mention\"],\"other\":[\"mentions\"]}]],\"email.digest.count.playlistFollows\":[[\"n\",\"plural\",{\"one\":[\"abonnement à vos collections\"],\"other\":[\"abonnements à vos collections\"]}]],\"email.digest.forYou\":[\"Pour vous\"],\"email.digest.hl.comment\":[[\"who\"],\" a commenté votre reco \",[\"target\"]],\"email.digest.hl.commentLike\":[[\"who\"],\" a aimé votre commentaire sur \",[\"target\"]],\"email.digest.hl.mentionComment\":[[\"who\"],\" vous a mentionné dans un commentaire sur \",[\"target\"]],\"email.digest.hl.mentionIn\":[[\"who\"],\" vous a mentionné dans \",[\"target\"]],\"email.digest.hl.mentionPlaylist\":[[\"who\"],\" vous a mentionné dans la collection \",[\"target\"]],\"email.digest.hl.playlistFollow\":[[\"who\"],\" a suivi votre collection \",[\"target\"]],\"email.digest.hl.userFollow\":[[\"who\"],\" vous suit désormais\"],\"email.digest.inChat\":[\"Dans la tribune\"],\"email.digest.monthly.reason\":[\"Vous recevez ce récap mensuel car il est activé dans vos paramètres.\"],\"email.digest.monthly.subject\":[\"Le mois sur \",[\"site\"],\", \",[\"range\"]],\"email.digest.monthly.title\":[\"Le mois sur \",[\"site\"]],\"email.digest.more\":[[\"n\"],\" de plus dans votre fil Suivi\"],\"email.digest.network\":[\"De ceux que vous suivez\"],\"email.digest.openChat\":[\"Ouvrir la tribune\"],\"email.digest.playlistMeta\":[\"par @\",[\"username\"],\" · \",[\"n\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}]],\"email.digest.pre.members\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouveau membre\"],\"other\":[\"#\",\" nouveaux membres\"]}]],\"email.digest.pre.network\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouvelle reco de ceux que vous suivez\"],\"other\":[\"#\",\" nouvelles recos de ceux que vous suivez\"]}]],\"email.digest.pre.playlists\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouvelle collection\"],\"other\":[\"#\",\" nouvelles collections\"]}]],\"email.digest.pre.topMonth\":[[\"n\",\"plural\",{\"one\":[\"la reco la plus votée du mois\"],\"other\":[\"les \",\"#\",\" recos les plus votées du mois\"]}]],\"email.digest.pre.topWeek\":[[\"n\",\"plural\",{\"one\":[\"la reco la plus votée de la semaine\"],\"other\":[\"les \",\"#\",\" recos les plus votées de la semaine\"]}]],\"email.digest.seeAll\":[[\"n\",\"plural\",{\"one\":[\"Voir la notification\"],\"other\":[\"Voir les \",\"#\",\" notifications\"]}]],\"email.digest.theChat\":[\"la tribune\"],\"email.digest.totals\":[[\"dumps\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}],\", \",[\"upvotes\",\"plural\",{\"one\":[\"#\",\" vote\"],\"other\":[\"#\",\" votes\"]}],\" et \",[\"comments\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}],\".\"],\"email.digest.upload\":[\"fichier\"],\"email.digest.weekly.reason\":[\"Vous recevez ce résumé hebdomadaire car il est activé dans vos paramètres.\"],\"email.digest.weekly.subject\":[\"Votre semaine sur \",[\"site\"],\", \",[\"range\"]],\"email.digest.weekly.title\":[\"Votre semaine, \",[\"username\"]],\"email.digest.yours\":[\"à vous\"],\"email.reset.body\":[\"Quelqu'un a demandé à réinitialiser le mot de passe de votre compte \",[\"site\"],\". Le bouton ci-dessous vous permet d'en choisir un nouveau. Il ne fonctionne qu'une fois, pendant \",[\"hours\",\"plural\",{\"one\":[\"#\",\" heure\"],\"other\":[\"#\",\" heures\"]}],\".\"],\"email.reset.button\":[\"Choisir un nouveau mot de passe\"],\"email.reset.fallback\":[\"Si le bouton ne fonctionne pas, collez ce lien dans votre navigateur :\"],\"email.reset.hi\":[\"Bonjour \",[\"username\"],\",\"],\"email.reset.ignore\":[\"Si vous n'êtes pas à l'origine de cette demande, ignorez cet e-mail. Votre mot de passe ne change pas.\"],\"email.reset.preheader\":[\"Le lien ne fonctionne qu'une fois, pendant \",[\"hours\",\"plural\",{\"one\":[\"#\",\" heure\"],\"other\":[\"#\",\" heures\"]}],\".\"],\"email.reset.reason\":[\"Vous recevez cet e-mail car une réinitialisation de mot de passe a été demandée pour cette adresse.\"],\"email.reset.subject\":[\"Réinitialisez votre mot de passe \",[\"site\"]],\"email.reset.title\":[\"Réinitialiser votre mot de passe\"],\"email.settings\":[\"Paramètres\"],\"email.welcome.body\":[\"# Bienvenue sur \",[\"site\"],\" !\\n\\nBonjour **\",[\"username\"],\"**,\\n\\nVotre compte a bien été créé. Bienvenue à bord !\"],\"email.welcome.button\":[\"Ouvrir \",[\"site\"]],\"email.welcome.digests\":[\"Chaque semaine, vous recevrez un résumé de ce qui s'est passé ici, et chaque mois un récap des recos les plus votées. Vous pouvez désactiver l'un ou l'autre dans vos paramètres.\"],\"email.welcome.preheader\":[\"Votre compte est prêt.\"],\"email.welcome.reason\":[\"Vous recevez cet e-mail car un compte \",[\"site\"],\" a été créé avec cette adresse.\"],\"email.welcome.subject\":[\"Bienvenue sur \",[\"site\"]],\"en9o7K\":[\"Impossible de publier le commentaire\"],\"etFQQS\":[\"Pourquoi on en voudrait ?\"],\"f3cEDO\":[\"Changer l'image\"],\"fC6mXb\":[\"Voter\"],\"fI-mNw\":[\"Collections\"],\"fgLNSM\":[\"S'inscrire\"],\"gANddk\":[\"Envoi…\"],\"gGx5tM\":[\"Modification\"],\"gIQQwD\":[\"Chargement échoué\"],\"gIpLuy\":[\"Il faut un lien\"],\"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\"],\" a publié <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\"],\"jTGs8A\":[\"Ce site n'envoie pas d'e-mails, aucun résumé ne part donc.\"],\"jbernk\":[\"Chargement du profil…\"],\"jnrIbz\":[\"Voté (\",[\"0\"],\")\"],\"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\"],\"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\"],\"mDJo8W\":[\"Recos (\",[\"0\"],\")\"],\"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\"]}]],\"oW3yYY\":[\"Ce mois-ci\"],\"ogtYkT\":[\"Mot de passe mis à jour\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" vous a mentionné dans <1>\",[\"where\"],\"\"],\"pSheLH\":[\"Aucun invité pour le moment.\"],\"pd3FCq\":[\"Ce fichier dépasse \",[\"limit\"],\".\"],\"pvnfJD\":[\"Sombre\"],\"qIMfNQ\":[\"Supprimer la collection\"],\"qItrqH\":[\"Vous ne recevrez plus le récap mensuel. Vous pouvez le réactiver dans vos paramètres.\"],\"qbDAcy\":[\"Recommander\"],\"qgx_78\":[\"Suivez des collections publiques pour voir leurs recos ici.\"],\"qnUJBI\":[\"Le nouvel ordre n'a pas pu être enregistré. Réessayez.\"],\"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\"],\"rn6SBY\":[\"Rétablir le son\"],\"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\"],\"tyiiHe\":[\"Rien de neuf ce mois-ci.\"],\"u1lDX2\":[\"Récupération de l'aperçu…\"],\"uD0qXQ\":[\"Déposez un fichier ici\"],\"uISnsY\":[\"Suppression impossible\"],\"uMGUnV\":[\"Pas encore de collections.\"],\"ub1EEL\":[\"modifié \",[\"0\"]],\"uomLas\":[\"Ajouter une catégorie\"],\"v-Wp--\":[\"Ouvrir la source\"],\"vJBF1r\":[\"Publication…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" non lues)\"],\"vQMkHu\":[\"Retirer le vote\"],\"vuosjb\":[\"Menu utilisateur\"],\"wckWOP\":[\"Administration\"],\"wixIgH\":[\"Vous avez déjà un compte ? <0>Se connecter\"],\"wtxFp4\":[\"Fichier trop volumineux (max \",[\"max\"],\").\"],\"x6tjuK\":[\"Onglet par défaut\"],\"xEWkgZ\":[\"← Retour à toutes les recos\"],\"xPHtx0\":[\"Lancer la recherche\"],\"xVuNgt\":[\"+ Nouvelle collection\"],\"xc9O_u\":[\"Supprimer la reco\"],\"y1G_DI\":[\"Renommage impossible\"],\"y6sq5j\":[\"Abonné\"],\"yA_6BX\":[\"Tout voir →\"],\"yBBtRm\":[\"Suivez des utilisateurs pour voir leurs recos ici.\"],\"yByRxz\":[\"Cette semaine\"],\"yQ2kGp\":[\"Charger plus\"],\"y_0uwd\":[\"Hier\"],\"yxlcv5\":[\"Ne plus recevoir le récap mensuel ?\"],\"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*/export const messages=JSON.parse("{\"-K9EZb\":[\"Ajouter un e-mail…\"],\"-Ya-b9\":[\"Enregistrement échoué\"],\"-iDCtG\":[\"Vos paramètres\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Rechercher des recos, utilisateurs, collections…\"],\"1QfxQT\":[\"Fermer\"],\"1YKhJ3\":[\"1 nouveau message\"],\"1njn7W\":[\"Clair\"],\"1utXA6\":[\"Recos\"],\"26iNma\":[\"Publier le commentaire\"],\"2DoBvq\":[\"Flux\"],\"2Hlmdt\":[\"Écrire une réponse…\"],\"2_DHgA\":[\"Identifiant d’URL\"],\"2nrsEn\":[\"message supprimé\"],\"2wxgft\":[\"Renommer\"],\"2ygf_L\":[\"← Retour\"],\"3KKSM4\":[\"privé\"],\"3QuNUr\":[\"Plusieurs personnes écrivent…\"],\"3SEwY1\":[\"Supprimer la catégorie \\\"\",[\"0\"],\"\\\" ? Cette action est irréversible.\"],\"3qkggm\":[\"Plein écran\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" a suivi votre collection <1>\",[\"1\"],\"\"],\"48e2R8\":[\"Modifier l'e-mail\"],\"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…\"],\"5NZ3tg\":[\"Désactivation…\"],\"5TviPn\":[\"Annuler la recherche\"],\"5cC8f2\":[\"Modifié le \",[\"0\"]],\"5oD9f_\":[\"Plus tôt\"],\"6Qly-0\":[\"un commentaire\"],\"6YE0P6\":[\"Édition de \",[\"0\"]],\"6YtxFj\":[\"Nom\"],\"6_dCYd\":[\"Aperçu\"],\"6gRgw8\":[\"Réessayer\"],\"7PHCIN\":[\"Annuler la suppression\"],\"7PzzBU\":[\"Utilisateur\"],\"7d1a0d\":[\"Public\"],\"7noMWp\":[\"Rien de neuf cette semaine.\"],\"7sNhEz\":[\"Nom d'utilisateur\"],\"8-8abB\":[\"Déconnecté — reconnexion…\"],\"8F1i42\":[\"Page introuvable\"],\"8JplJJ\":[\"Désabonné\"],\"8Ug9jB\":[\"Connexe\"],\"8ZsakT\":[\"Mot de passe\"],\"92o1-U\":[\"Suppression échouée\"],\"9BruTc\":[\"Pourquoi ?\"],\"9l4qcT\":[\"Déposez un fichier de remplacement ici\"],\"9uI_rE\":[\"Annuler\"],\"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\"],\"AhNP40\":[\"Position\"],\"AnRu_j\":[\"Annuler la réponse\"],\"B4LeYP\":[\"Échec de la suppression (\",[\"0\"],\")\"],\"BjitBo\":[\"Télécharger \",[\"0\"]],\"BlpJ2G\":[\"Reprise de votre brouillon.\"],\"CI50ct\":[\"Voté\"],\"C_qQE4\":[\"Déjà recommandé par \",[\"0\"],\" \",[\"1\"]],\"Cf_gxi\":[\"Ne plus recevoir le résumé hebdomadaire ?\"],\"Cj24wt\":[\"Inscription…\"],\"CnqayG\":[\"La mise à jour a échoué. Réessayez.\"],\"D8DRYV\":[\"Supprimer ce message ?\"],\"DI3NBO\":[\"Ouvrir sur \",[\"0\"]],\"DPfwMq\":[\"Terminé\"],\"DdeHXH\":[\"Supprimer cette reco ? Cette action est irréversible.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" a voté pour <1>\",[\"1\"],\"\"],\"E417RG\":[\"Vous ne recevrez plus le résumé hebdomadaire. Vous pouvez le réactiver dans vos paramètres.\"],\"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é.\"],\"EwVrWF\":[\"Le dump n'a pas pu être retiré. Réessayez.\"],\"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\"],\"Gv5nA5\":[\"Format de réponse inattendu\"],\"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\"],\"IF7RiR\":[\"Couverture\"],\"IO9KL_\":[\"Piste suivante\"],\"IZX7TO\":[\"Impossible de générer une invitation\"],\"IagCbF\":[\"URL\"],\"IgrLD_\":[\"Pause\"],\"IhK90M\":[\"Lettres, chiffres et tirets bas seulement\"],\"ImOQa9\":[\"Répondre\"],\"JJ-Bhk\":[\"Votre adresse e-mail\"],\"JLasL-\":[\"Désabonnement impossible\"],\"JRQitQ\":[\"Confirmer le nouveau mot de passe\"],\"JXr41k\":[\"<0>\",[\"0\"],\" a commenté sur <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…\"],\"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\"],\"Nu4oKW\":[\"Description\"],\"O32mmL\":[\"Modifier la description\"],\"O3oNi5\":[\"E-mail\"],\"Oprv1v\":[\"Mot de passe (min. \",[\"0\"],\" caractères)\"],\"P5MuiB\":[\"Ajouter une image\"],\"PJTy3N\":[[\"totalUnread\",\"plural\",{\"one\":[\"#\",\" nouvelle\"],\"other\":[\"#\",\" nouvelles\"]}]],\"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 fichiers\"],\"QLtPBd\":[\"Aucune reco dans cette collection pour l'instant.\"],\"QctOgc\":[\"Impossible de joindre le serveur. Vérifiez votre connexion et réessayez.\"],\"R7SsBE\":[\"Couper le son\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Supprimer cette collection ? Cette action est irréversible.\"],\"RTksSy\":[\"<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…\"],\"SMaFdc\":[\"Se désabonner\"],\"Sad2tK\":[\"Envoi…\"],\"So7gCD\":[\"Collez un lien…\"],\"StovX6\":[\"Rien à voir, circulez.\"],\"Sxm8rQ\":[\"Utilisateurs\"],\"T8ejtZ\":[\"Ce commentaire n'a pas pu être supprimé. Réessayez.\"],\"T91vKp\":[\"Lecture\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" a été ajouté à <1>\",[\"1\"],\"\"],\"TAtbho\":[\"Chargement des messages…\"],\"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\"],\"V38P77\":[\"Seuls son auteur et les modérateurs peuvent modifier ce dump.\"],\"VCoEm-\":[\"Retour à la connexion\"],\"VKsaTi\":[\"En réponse à\"],\"VLAd8f\":[\"Max \",[\"limit\"]],\"V_e7nf\":[\"Définir un nouveau mot de passe\"],\"VnNJbN\":[\"De collections\"],\"VskHIx\":[\"Supprimer la catégorie\"],\"VyTYmS\":[\"Changer l'avatar\"],\"W5A0Ly\":[\"Une erreur inattendue s'est produite.\"],\"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\"],\"XnL-Eu\":[\"Aucun utilisateur ne correspond à « \",[\"q\"],\" ».\"],\"Xs2Lez\":[\"Ce lien de réinitialisation est absent ou malformé.\"],\"YK1Dhc\":[\"une publication\"],\"YWnSuq\":[\"Ce lien de désabonnement est absent ou mal formé.\"],\"YZxXK8\":[\"Fermer le sélecteur d'emoji\"],\"YcjhhX\":[\"Gestion utilisateur\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" a aimé votre commentaire sur <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…\"],\"_I1_HN\":[\"Minuscules, chiffres et tirets simples seulement\"],\"_R_sGB\":[\"Mot de passe modifié avec succès.\"],\"_aept4\":[\"Publier la réponse\"],\"_cF7Rs\":[\"Volume\"],\"_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…\"],\"b_1P1D\":[\"Renommage…\"],\"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\"],\"digest.community\":[\"Communauté\"],\"digest.count.comments\":[[\"n\",\"plural\",{\"one\":[\"commentaire\"],\"other\":[\"commentaires\"]}]],\"digest.count.upvotes\":[[\"n\",\"plural\",{\"one\":[\"vote\"],\"other\":[\"votes\"]}]],\"digest.invitedBy\":[\"sur invitation de @\",[\"username\"]],\"digest.invitedByYou\":[\"sur votre invitation\"],\"digest.monthly\":[\"Récap mensuel\"],\"digest.monthly.off\":[\"Ne plus recevoir le récap mensuel\"],\"digest.newMembers\":[\"Nouveaux membres\"],\"digest.newPlaylists\":[\"Nouvelles collections publiques\"],\"digest.stat.chat\":[[\"n\",\"plural\",{\"one\":[\"message dans la tribune\"],\"other\":[\"messages dans la tribune\"]}]],\"digest.stat.dumps\":[[\"n\",\"plural\",{\"one\":[\"reco\"],\"other\":[\"recos\"]}]],\"digest.topMonth\":[\"Les plus votées du mois\"],\"digest.topWeek\":[\"Les plus votées de la semaine\"],\"digest.weekly\":[\"Résumé hebdomadaire\"],\"digest.weekly.off\":[\"Ne plus recevoir le résumé hebdomadaire\"],\"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.\"],\"email.digest.addedTo\":[\"ajoutée à \",[\"playlist\"]],\"email.digest.andMore\":[\"et \",[\"n\"],\" de plus\"],\"email.digest.by\":[\"par @\",[\"username\"]],\"email.digest.chatWeek\":[[\"n\",\"plural\",{\"one\":[\"#\",\" message cette semaine\"],\"other\":[\"#\",\" messages cette semaine\"]}]],\"email.digest.communityPage\":[\"Tout voir sur la page de la communauté\"],\"email.digest.count.followers\":[[\"n\",\"plural\",{\"one\":[\"nouvel abonné\"],\"other\":[\"nouveaux abonnés\"]}]],\"email.digest.count.likes\":[[\"n\",\"plural\",{\"one\":[\"j'aime sur vos commentaires\"],\"other\":[\"j'aime sur vos commentaires\"]}]],\"email.digest.count.mentions\":[[\"n\",\"plural\",{\"one\":[\"mention\"],\"other\":[\"mentions\"]}]],\"email.digest.count.playlistFollows\":[[\"n\",\"plural\",{\"one\":[\"abonnement à vos collections\"],\"other\":[\"abonnements à vos collections\"]}]],\"email.digest.forYou\":[\"Pour vous\"],\"email.digest.hl.comment\":[[\"who\"],\" a commenté votre reco \",[\"target\"]],\"email.digest.hl.commentLike\":[[\"who\"],\" a aimé votre commentaire sur \",[\"target\"]],\"email.digest.hl.mentionComment\":[[\"who\"],\" vous a mentionné dans un commentaire sur \",[\"target\"]],\"email.digest.hl.mentionEveryone\":[[\"who\"],\" a mentionné tout le monde dans \",[\"target\"]],\"email.digest.hl.mentionIn\":[[\"who\"],\" vous a mentionné dans \",[\"target\"]],\"email.digest.hl.mentionPlaylist\":[[\"who\"],\" vous a mentionné dans la collection \",[\"target\"]],\"email.digest.hl.playlistFollow\":[[\"who\"],\" a suivi votre collection \",[\"target\"]],\"email.digest.hl.userFollow\":[[\"who\"],\" vous suit désormais\"],\"email.digest.inChat\":[\"Dans la tribune\"],\"email.digest.monthly.reason\":[\"Vous recevez ce récap mensuel car il est activé dans vos paramètres.\"],\"email.digest.monthly.subject\":[\"Le mois sur \",[\"site\"],\", \",[\"range\"]],\"email.digest.monthly.title\":[\"Le mois sur \",[\"site\"]],\"email.digest.more\":[[\"n\"],\" de plus dans votre fil Suivi\"],\"email.digest.network\":[\"De ceux que vous suivez\"],\"email.digest.openChat\":[\"Ouvrir la tribune\"],\"email.digest.playlistMeta\":[\"par @\",[\"username\"],\" · \",[\"n\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}]],\"email.digest.pre.members\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouveau membre\"],\"other\":[\"#\",\" nouveaux membres\"]}]],\"email.digest.pre.network\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouvelle reco de ceux que vous suivez\"],\"other\":[\"#\",\" nouvelles recos de ceux que vous suivez\"]}]],\"email.digest.pre.playlists\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouvelle collection\"],\"other\":[\"#\",\" nouvelles collections\"]}]],\"email.digest.pre.topMonth\":[[\"n\",\"plural\",{\"one\":[\"la reco la plus votée du mois\"],\"other\":[\"les \",\"#\",\" recos les plus votées du mois\"]}]],\"email.digest.pre.topWeek\":[[\"n\",\"plural\",{\"one\":[\"la reco la plus votée de la semaine\"],\"other\":[\"les \",\"#\",\" recos les plus votées de la semaine\"]}]],\"email.digest.seeAll\":[[\"n\",\"plural\",{\"one\":[\"Voir la notification\"],\"other\":[\"Voir les \",\"#\",\" notifications\"]}]],\"email.digest.theChat\":[\"la tribune\"],\"email.digest.totals\":[[\"dumps\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}],\", \",[\"upvotes\",\"plural\",{\"one\":[\"#\",\" vote\"],\"other\":[\"#\",\" votes\"]}],\" et \",[\"comments\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}],\".\"],\"email.digest.upload\":[\"fichier\"],\"email.digest.weekly.reason\":[\"Vous recevez ce résumé hebdomadaire car il est activé dans vos paramètres.\"],\"email.digest.weekly.subject\":[\"Votre semaine sur \",[\"site\"],\", \",[\"range\"]],\"email.digest.weekly.title\":[\"Votre semaine, \",[\"username\"]],\"email.digest.yours\":[\"à vous\"],\"email.reset.body\":[\"Quelqu'un a demandé à réinitialiser le mot de passe de votre compte \",[\"site\"],\". Le bouton ci-dessous vous permet d'en choisir un nouveau. Il ne fonctionne qu'une fois, pendant \",[\"hours\",\"plural\",{\"one\":[\"#\",\" heure\"],\"other\":[\"#\",\" heures\"]}],\".\"],\"email.reset.button\":[\"Choisir un nouveau mot de passe\"],\"email.reset.fallback\":[\"Si le bouton ne fonctionne pas, collez ce lien dans votre navigateur :\"],\"email.reset.hi\":[\"Bonjour \",[\"username\"],\",\"],\"email.reset.ignore\":[\"Si vous n'êtes pas à l'origine de cette demande, ignorez cet e-mail. Votre mot de passe ne change pas.\"],\"email.reset.preheader\":[\"Le lien ne fonctionne qu'une fois, pendant \",[\"hours\",\"plural\",{\"one\":[\"#\",\" heure\"],\"other\":[\"#\",\" heures\"]}],\".\"],\"email.reset.reason\":[\"Vous recevez cet e-mail car une réinitialisation de mot de passe a été demandée pour cette adresse.\"],\"email.reset.subject\":[\"Réinitialisez votre mot de passe \",[\"site\"]],\"email.reset.title\":[\"Réinitialiser votre mot de passe\"],\"email.settings\":[\"Paramètres\"],\"email.welcome.body\":[\"# Bienvenue sur \",[\"site\"],\" !\\n\\nBonjour **\",[\"username\"],\"**,\\n\\nVotre compte a bien été créé. Bienvenue à bord !\"],\"email.welcome.button\":[\"Ouvrir \",[\"site\"]],\"email.welcome.digests\":[\"Chaque semaine, vous recevrez un résumé de ce qui s'est passé ici, et chaque mois un récap des recos les plus votées. Vous pouvez désactiver l'un ou l'autre dans vos paramètres.\"],\"email.welcome.preheader\":[\"Votre compte est prêt.\"],\"email.welcome.reason\":[\"Vous recevez cet e-mail car un compte \",[\"site\"],\" a été créé avec cette adresse.\"],\"email.welcome.subject\":[\"Bienvenue sur \",[\"site\"]],\"en9o7K\":[\"Impossible de publier le commentaire\"],\"etFQQS\":[\"Pourquoi on en voudrait ?\"],\"f3cEDO\":[\"Changer l'image\"],\"fC6mXb\":[\"Voter\"],\"fI-mNw\":[\"Collections\"],\"fgLNSM\":[\"S'inscrire\"],\"gANddk\":[\"Envoi…\"],\"gGx5tM\":[\"Modification\"],\"gIQQwD\":[\"Chargement échoué\"],\"gIpLuy\":[\"Il faut un lien\"],\"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\"],\" a publié <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\"],\"jTGs8A\":[\"Ce site n'envoie pas d'e-mails, aucun résumé ne part donc.\"],\"jbernk\":[\"Chargement du profil…\"],\"jnrIbz\":[\"Voté (\",[\"0\"],\")\"],\"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\"],\"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\"],\"mDJo8W\":[\"Recos (\",[\"0\"],\")\"],\"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…\"],\"nucyoV\":[\"<0>\",[\"0\"],\" a mentionné tout le monde dans <1>\",[\"where\"],\"\"],\"nwtY4N\":[\"Une erreur est survenue\"],\"nx4kaN\":[\"Sauvegarde impossible\"],\"oTYvvN\":[[\"0\",\"plural\",{\"one\":[\"et \",\"#\",\" reco plus ancienne\"],\"other\":[\"et \",\"#\",\" recos plus anciennes\"]}]],\"oW3yYY\":[\"Ce mois-ci\"],\"ogtYkT\":[\"Mot de passe mis à jour\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" vous a mentionné dans <1>\",[\"where\"],\"\"],\"pSheLH\":[\"Aucun invité pour le moment.\"],\"pd3FCq\":[\"Ce fichier dépasse \",[\"limit\"],\".\"],\"pvnfJD\":[\"Sombre\"],\"qIMfNQ\":[\"Supprimer la collection\"],\"qItrqH\":[\"Vous ne recevrez plus le récap mensuel. Vous pouvez le réactiver dans vos paramètres.\"],\"qbDAcy\":[\"Recommander\"],\"qgx_78\":[\"Suivez des collections publiques pour voir leurs recos ici.\"],\"qnUJBI\":[\"Le nouvel ordre n'a pas pu être enregistré. Réessayez.\"],\"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\"],\"rn6SBY\":[\"Rétablir le son\"],\"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\"],\"tyiiHe\":[\"Rien de neuf ce mois-ci.\"],\"u1lDX2\":[\"Récupération de l'aperçu…\"],\"uD0qXQ\":[\"Déposez un fichier ici\"],\"uISnsY\":[\"Suppression impossible\"],\"uMGUnV\":[\"Pas encore de collections.\"],\"ub1EEL\":[\"modifié \",[\"0\"]],\"uomLas\":[\"Ajouter une catégorie\"],\"v-Wp--\":[\"Ouvrir la source\"],\"vJBF1r\":[\"Publication…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" non lues)\"],\"vQMkHu\":[\"Retirer le vote\"],\"vuosjb\":[\"Menu utilisateur\"],\"wckWOP\":[\"Administration\"],\"wixIgH\":[\"Vous avez déjà un compte ? <0>Se connecter\"],\"wtxFp4\":[\"Fichier trop volumineux (max \",[\"max\"],\").\"],\"x6tjuK\":[\"Onglet par défaut\"],\"xEWkgZ\":[\"← Retour à toutes les recos\"],\"xPHtx0\":[\"Lancer la recherche\"],\"xVuNgt\":[\"+ Nouvelle collection\"],\"xc9O_u\":[\"Supprimer la reco\"],\"y1G_DI\":[\"Renommage impossible\"],\"y6sq5j\":[\"Abonné\"],\"yA_6BX\":[\"Tout voir →\"],\"yBBtRm\":[\"Suivez des utilisateurs pour voir leurs recos ici.\"],\"yByRxz\":[\"Cette semaine\"],\"yQ2kGp\":[\"Charger plus\"],\"y_0uwd\":[\"Hier\"],\"yxlcv5\":[\"Ne plus recevoir le récap mensuel ?\"],\"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/locales/fr.po b/locales/fr.po index 948c2045..1bee4efe 100644 --- a/locales/fr.po +++ b/locales/fr.po @@ -14,175 +14,180 @@ msgstr "" "Language-Team: \n" #. js-lingui-explicit-id -#: api/mail/digest.ts:129 +#: api/mail/digest.ts:163 +msgid "email.digest.hl.mentionEveryone" +msgstr "{who} a mentionné tout le monde dans {target}" + +#. js-lingui-explicit-id +#: api/mail/digest.ts:130 #: src/pages/Community.tsx:49 msgid "digest.count.upvotes" msgstr "{n, plural, one {vote} other {votes}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:131 +#: api/mail/digest.ts:132 msgid "email.digest.count.followers" msgstr "{n, plural, one {nouvel abonné} other {nouveaux abonnés}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:133 +#: api/mail/digest.ts:134 #: src/pages/Community.tsx:50 msgid "digest.count.comments" msgstr "{n, plural, one {commentaire} other {commentaires}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:135 +#: api/mail/digest.ts:136 msgid "email.digest.count.mentions" msgstr "{n, plural, one {mention} other {mentions}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:137 +#: api/mail/digest.ts:138 msgid "email.digest.count.likes" msgstr "{n, plural, one {j'aime sur vos commentaires} other {j'aime sur vos commentaires}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:139 +#: api/mail/digest.ts:140 msgid "email.digest.count.playlistFollows" msgstr "{n, plural, one {abonnement à vos collections} other {abonnements à vos collections}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:155 +#: api/mail/digest.ts:156 msgid "email.digest.hl.mentionComment" msgstr "{who} vous a mentionné dans un commentaire sur {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:158 +#: api/mail/digest.ts:159 msgid "email.digest.hl.mentionIn" msgstr "{who} vous a mentionné dans {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:160 +#: api/mail/digest.ts:161 msgid "email.digest.hl.mentionPlaylist" msgstr "{who} vous a mentionné dans la collection {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:162 +#: api/mail/digest.ts:165 msgid "email.digest.hl.comment" msgstr "{who} a commenté votre reco {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:164 +#: api/mail/digest.ts:167 msgid "email.digest.hl.playlistFollow" msgstr "{who} a suivi votre collection {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:166 +#: api/mail/digest.ts:169 msgid "email.digest.hl.userFollow" msgstr "{who} vous suit désormais" #. js-lingui-explicit-id -#: api/mail/digest.ts:168 +#: api/mail/digest.ts:171 msgid "email.digest.hl.commentLike" msgstr "{who} a aimé votre commentaire sur {target}" #. js-lingui-explicit-id -#: api/mail/digest.ts:172 +#: api/mail/digest.ts:175 msgid "email.digest.by" msgstr "par @{username}" #. js-lingui-explicit-id -#: api/mail/digest.ts:174 +#: api/mail/digest.ts:177 msgid "email.digest.addedTo" msgstr "ajoutée à {playlist}" #. js-lingui-explicit-id -#: api/mail/digest.ts:175 +#: api/mail/digest.ts:178 msgid "email.digest.upload" msgstr "fichier" #. js-lingui-explicit-id -#: api/mail/digest.ts:178 +#: api/mail/digest.ts:181 #: src/pages/Community.tsx:141 msgid "digest.invitedByYou" msgstr "sur votre invitation" #. js-lingui-explicit-id -#: api/mail/digest.ts:179 +#: api/mail/digest.ts:182 #: src/pages/Community.tsx:143 msgid "digest.invitedBy" msgstr "sur invitation de @{username}" #. js-lingui-explicit-id -#: api/mail/digest.ts:180 +#: api/mail/digest.ts:183 msgid "email.digest.andMore" msgstr "et {n} de plus" #. js-lingui-explicit-id -#: api/mail/digest.ts:182 +#: api/mail/digest.ts:185 msgid "email.digest.playlistMeta" msgstr "par @{username} · {n, plural, one {# reco} other {# recos}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:205 +#: api/mail/digest.ts:208 msgid "email.digest.yours" msgstr "à vous" #. js-lingui-explicit-id -#: api/mail/digest.ts:223 +#: api/mail/digest.ts:226 #: src/pages/Settings.tsx:154 msgid "digest.monthly" msgstr "Récap mensuel" #. js-lingui-explicit-id -#: api/mail/digest.ts:223 +#: api/mail/digest.ts:226 #: src/pages/Settings.tsx:145 msgid "digest.weekly" msgstr "Résumé hebdomadaire" #. js-lingui-explicit-id -#: api/mail/digest.ts:231 +#: api/mail/digest.ts:234 msgid "email.digest.monthly.title" msgstr "Le mois sur {site}" #. js-lingui-explicit-id -#: api/mail/digest.ts:232 +#: api/mail/digest.ts:235 msgid "email.digest.weekly.title" msgstr "Votre semaine, {username}" #. js-lingui-explicit-id -#: api/mail/digest.ts:240 +#: api/mail/digest.ts:243 msgid "email.digest.forYou" msgstr "Pour vous" #. js-lingui-explicit-id -#: api/mail/digest.ts:249 +#: api/mail/digest.ts:252 msgid "email.digest.theChat" msgstr "la tribune" #. js-lingui-explicit-id -#: api/mail/digest.ts:264 +#: api/mail/digest.ts:267 msgid "email.digest.seeAll" msgstr "{n, plural, one {Voir la notification} other {Voir les # notifications}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:273 +#: api/mail/digest.ts:276 msgid "email.digest.network" msgstr "De ceux que vous suivez" #. js-lingui-explicit-id -#: api/mail/digest.ts:279 +#: api/mail/digest.ts:282 msgid "email.digest.more" msgstr "{n} de plus dans votre fil Suivi" #. js-lingui-explicit-id -#: api/mail/digest.ts:291 +#: api/mail/digest.ts:294 #: src/pages/Community.tsx:127 msgid "digest.topMonth" msgstr "Les plus votées du mois" #. js-lingui-explicit-id -#: api/mail/digest.ts:291 +#: api/mail/digest.ts:294 #: src/pages/Community.tsx:126 msgid "digest.topWeek" msgstr "Les plus votées de la semaine" #. js-lingui-explicit-id -#: api/mail/digest.ts:307 +#: api/mail/digest.ts:310 #: src/components/layout/UserMenu.tsx:94 #: src/pages/Community.tsx:73 #: src/pages/Community.tsx:100 @@ -190,113 +195,113 @@ msgid "digest.community" msgstr "Communauté" #. js-lingui-explicit-id -#: api/mail/digest.ts:314 +#: api/mail/digest.ts:317 #: src/pages/Community.tsx:48 msgid "digest.stat.dumps" msgstr "{n, plural, one {reco} other {recos}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:320 +#: api/mail/digest.ts:323 #: src/pages/Community.tsx:53 msgid "digest.stat.chat" msgstr "{n, plural, one {message dans la tribune} other {messages dans la tribune}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:329 +#: api/mail/digest.ts:332 #: src/pages/Community.tsx:133 msgid "digest.newMembers" msgstr "Nouveaux membres" #. js-lingui-explicit-id -#: api/mail/digest.ts:348 +#: api/mail/digest.ts:351 #: src/pages/Community.tsx:154 msgid "digest.newPlaylists" msgstr "Nouvelles collections publiques" #. js-lingui-explicit-id -#: api/mail/digest.ts:363 +#: api/mail/digest.ts:366 msgid "email.digest.inChat" msgstr "Dans la tribune" #. js-lingui-explicit-id -#: api/mail/digest.ts:365 +#: api/mail/digest.ts:368 msgid "email.digest.chatWeek" msgstr "{n, plural, one {# message cette semaine} other {# messages cette semaine}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:368 +#: api/mail/digest.ts:371 msgid "email.digest.openChat" msgstr "Ouvrir la tribune" #. js-lingui-explicit-id -#: api/mail/digest.ts:377 +#: api/mail/digest.ts:380 msgid "email.digest.totals" msgstr "{dumps, plural, one {# reco} other {# recos}}, {upvotes, plural, one {# vote} other {# votes}} et {comments, plural, one {# commentaire} other {# commentaires}}." #. js-lingui-explicit-id -#: api/mail/digest.ts:383 +#: api/mail/digest.ts:386 msgid "email.digest.communityPage" msgstr "Tout voir sur la page de la communauté" #. js-lingui-explicit-id -#: api/mail/digest.ts:393 +#: api/mail/digest.ts:396 msgid "email.digest.monthly.reason" msgstr "Vous recevez ce récap mensuel car il est activé dans vos paramètres." #. js-lingui-explicit-id -#: api/mail/digest.ts:394 +#: api/mail/digest.ts:397 msgid "email.digest.weekly.reason" msgstr "Vous recevez ce résumé hebdomadaire car il est activé dans vos paramètres." #. js-lingui-explicit-id -#: api/mail/digest.ts:398 +#: api/mail/digest.ts:401 #: src/pages/Unsubscribe.tsx:118 msgid "digest.monthly.off" msgstr "Ne plus recevoir le récap mensuel" #. js-lingui-explicit-id -#: api/mail/digest.ts:398 +#: api/mail/digest.ts:401 #: src/pages/Unsubscribe.tsx:117 msgid "digest.weekly.off" msgstr "Ne plus recevoir le résumé hebdomadaire" #. js-lingui-explicit-id -#: api/mail/digest.ts:403 +#: api/mail/digest.ts:406 msgid "email.settings" msgstr "Paramètres" #. js-lingui-explicit-id -#: api/mail/digest.ts:411 +#: api/mail/digest.ts:414 msgid "email.digest.monthly.subject" msgstr "Le mois sur {site}, {range}" #. js-lingui-explicit-id -#: api/mail/digest.ts:415 +#: api/mail/digest.ts:418 msgid "email.digest.weekly.subject" msgstr "Votre semaine sur {site}, {range}" #. js-lingui-explicit-id -#: api/mail/digest.ts:438 +#: api/mail/digest.ts:441 msgid "email.digest.pre.network" msgstr "{n, plural, one {# nouvelle reco de ceux que vous suivez} other {# nouvelles recos de ceux que vous suivez}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:447 +#: api/mail/digest.ts:450 msgid "email.digest.pre.topMonth" msgstr "{n, plural, one {la reco la plus votée du mois} other {les # recos les plus votées du mois}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:451 +#: api/mail/digest.ts:454 msgid "email.digest.pre.members" msgstr "{n, plural, one {# nouveau membre} other {# nouveaux membres}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:454 +#: api/mail/digest.ts:457 msgid "email.digest.pre.playlists" msgstr "{n, plural, one {# nouvelle collection} other {# nouvelles collections}}" #. js-lingui-explicit-id -#: api/mail/digest.ts:458 +#: api/mail/digest.ts:461 msgid "email.digest.pre.topWeek" msgstr "{n, plural, one {la reco la plus votée de la semaine} other {les # recos les plus votées de la semaine}}" @@ -401,12 +406,12 @@ msgstr "{0, plural, one {et # reco plus ancienne} other {et # recos plus ancienn #. placeholder {0}: typingNames[0] #. placeholder {1}: typingNames[1] -#: src/components/chat/ChatModal.tsx:649 +#: src/components/chat/ChatModal.tsx:650 msgid "{0} and {1} are typing…" msgstr "{0} et {1} sont en train d'écrire…" #. placeholder {0}: typingNames[0] -#: src/components/chat/ChatModal.tsx:647 +#: src/components/chat/ChatModal.tsx:648 msgid "{0} is typing…" msgstr "{0} est en train d'écrire…" @@ -425,11 +430,11 @@ msgstr "{0}–{1} caractères : lettres, chiffres ou tirets bas" msgid "{label} ({count})" msgstr "{label} ({count})" -#: src/pages/Notifications.tsx:499 +#: src/pages/Notifications.tsx:508 msgid "{totalUnread, plural, one {# new} other {# new}}" msgstr "{totalUnread, plural, one {# nouvelle} other {# nouvelles}}" -#: src/components/chat/ChatModal.tsx:778 +#: src/components/chat/ChatModal.tsx:779 msgid "{unseenCount} new messages" msgstr "{unseenCount} nouveaux messages" @@ -480,7 +485,12 @@ msgid "<0>{0} liked your comment on <1>{1}" msgstr "<0>{0} a aimé votre commentaire sur <1>{1}" #. placeholder {0}: d.mentionerUsername -#: src/pages/Notifications.tsx:265 +#: src/pages/Notifications.tsx:266 +msgid "<0>{0} mentioned everyone in <1>{where}" +msgstr "<0>{0} a mentionné tout le monde dans <1>{where}" + +#. placeholder {0}: d.mentionerUsername +#: src/pages/Notifications.tsx:274 msgid "<0>{0} mentioned you in <1>{where}" msgstr "<0>{0} vous a mentionné dans <1>{where}" @@ -507,7 +517,7 @@ msgstr "<0>{0} a voté pour <1>{1}" msgid "<0>{0} was added to <1>{1}" msgstr "<0>{0} a été ajouté à <1>{1}" -#: src/components/chat/ChatModal.tsx:777 +#: src/components/chat/ChatModal.tsx:778 msgid "1 new message" msgstr "1 nouveau message" @@ -597,7 +607,7 @@ msgid "Back to login" msgstr "Retour à la connexion" #: src/contexts/ws/WSProvider.tsx:395 -#: src/contexts/ws/WSProvider.tsx:761 +#: src/contexts/ws/WSProvider.tsx:764 msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects." msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion." @@ -620,8 +630,8 @@ msgstr "Annuler" msgid "Cancel removal" msgstr "Annuler la suppression" -#: src/components/chat/ChatModal.tsx:838 #: src/components/chat/ChatModal.tsx:839 +#: src/components/chat/ChatModal.tsx:840 msgid "Cancel reply" msgstr "Annuler la réponse" @@ -659,7 +669,7 @@ msgstr "Changer le mot de passe…" msgid "Change skin tone" msgstr "Changer la couleur de peau" -#: src/components/chat/ChatModal.tsx:738 +#: src/components/chat/ChatModal.tsx:739 #: src/components/layout/ChatButton.tsx:30 #: src/components/layout/ChatFab.tsx:54 #: src/components/layout/ChatFab.tsx:55 @@ -709,7 +719,7 @@ msgstr "Thème de couleur" msgid "Confirm new password" msgstr "Confirmer le nouveau mot de passe" -#: src/contexts/ws/WSProvider.tsx:754 +#: src/contexts/ws/WSProvider.tsx:757 msgid "Connection lost — this message may not have been sent" msgstr "Connexion perdue — ce message n'a peut-être pas été envoyé" @@ -746,7 +756,7 @@ 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/chat/ChatModal.tsx:664 +#: src/components/chat/ChatModal.tsx:665 msgid "Couldn't load the chat." msgstr "Impossible de charger le chat." @@ -788,9 +798,9 @@ msgstr "Onglet par défaut" #: src/components/category/CategoryManager.tsx:236 #: src/components/category/CategoryManager.tsx:237 -#: src/components/chat/ChatMessageItem.tsx:115 -#: src/components/chat/ChatMessageItem.tsx:116 -#: src/components/chat/ChatMessageItem.tsx:214 +#: src/components/chat/ChatMessageItem.tsx:118 +#: src/components/chat/ChatMessageItem.tsx:119 +#: src/components/chat/ChatMessageItem.tsx:220 #: src/components/comment/CommentNode.tsx:265 #: src/components/comment/CommentNode.tsx:271 #: src/components/form/DeleteButton.tsx:36 @@ -832,7 +842,7 @@ msgstr "Supprimer ce commentaire ?" msgid "Delete this dump? This cannot be undone." msgstr "Supprimer cette reco ? Cette action est irréversible." -#: src/components/chat/ChatMessageItem.tsx:213 +#: src/components/chat/ChatMessageItem.tsx:219 msgid "Delete this message?" msgstr "Supprimer ce message ?" @@ -857,12 +867,12 @@ msgstr "Description (facultatif)" msgid "Discard" msgstr "Abandonner" -#: src/components/chat/ChatModal.tsx:742 +#: src/components/chat/ChatModal.tsx:743 msgid "Disconnected — reconnecting…" msgstr "Déconnecté — reconnexion…" -#: src/components/chat/ChatModal.tsx:814 #: src/components/chat/ChatModal.tsx:815 +#: src/components/chat/ChatModal.tsx:816 msgid "Dismiss" msgstr "Fermer" @@ -910,12 +920,12 @@ msgstr "Recos" msgid "Dumps ({0})" msgstr "Recos ({0})" -#: src/pages/Notifications.tsx:305 +#: src/pages/Notifications.tsx:314 msgid "Earlier" msgstr "Plus tôt" -#: src/components/chat/ChatMessageItem.tsx:104 -#: src/components/chat/ChatMessageItem.tsx:105 +#: src/components/chat/ChatMessageItem.tsx:107 +#: src/components/chat/ChatMessageItem.tsx:108 #: src/components/chat/PendingItem.tsx:93 #: src/components/comment/CommentNode.tsx:256 #: src/components/playlist/PlaylistHeader.tsx:59 @@ -941,14 +951,14 @@ msgstr "Modifier l'e-mail" msgid "Edit title" msgstr "Modifier le titre" -#: src/components/chat/ChatMessageItem.tsx:128 +#: src/components/chat/ChatMessageItem.tsx:131 msgid "edited" msgstr "modifié" #. placeholder {0}: relativeTime(comment.updatedAt) #. placeholder {0}: relativeTime(dump.updatedAt) #. placeholder {0}: relativeTime(message.updatedAt) -#: src/components/chat/ChatMessageItem.tsx:129 +#: src/components/chat/ChatMessageItem.tsx:132 #: src/components/comment/CommentNode.tsx:205 #: src/components/playlist/PlaylistHeader.tsx:94 #: src/pages/Dump.tsx:505 @@ -958,7 +968,7 @@ msgstr "modifié {0}" #. placeholder {0}: comment.updatedAt.toLocaleString() #. placeholder {0}: dump.updatedAt.toLocaleString() #. placeholder {0}: message.updatedAt.toLocaleString() -#: src/components/chat/ChatMessageItem.tsx:125 +#: src/components/chat/ChatMessageItem.tsx:128 #: src/components/comment/CommentNode.tsx:203 #: src/components/playlist/PlaylistHeader.tsx:92 #: src/pages/Dump.tsx:503 @@ -1132,8 +1142,8 @@ msgstr "image" msgid "In collections" msgstr "Dans les collections" -#: src/components/editor/TextEditor.tsx:573 -#: src/components/editor/TextEditor.tsx:575 +#: src/components/editor/TextEditor.tsx:579 +#: src/components/editor/TextEditor.tsx:581 msgid "Insert emoji" msgstr "Insérer un emoji" @@ -1190,7 +1200,7 @@ msgstr "Lien" msgid "Link or file" msgstr "Lien ou fichier" -#: src/contexts/ws/WSProvider.tsx:760 +#: src/contexts/ws/WSProvider.tsx:763 msgid "Live updates are temporarily disconnected. Trying to reconnect…" msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…" @@ -1202,7 +1212,7 @@ msgstr "Mises à jour en direct indisponibles." msgid "Load more" msgstr "Charger plus" -#: src/components/chat/ChatModal.tsx:763 +#: src/components/chat/ChatModal.tsx:764 msgid "Load older messages" msgstr "Charger les messages plus anciens" @@ -1211,7 +1221,7 @@ msgstr "Charger les messages plus anciens" msgid "Loading dump…" msgstr "Chargement de la reco…" -#: src/components/chat/ChatModal.tsx:656 +#: src/components/chat/ChatModal.tsx:657 msgid "Loading messages…" msgstr "Chargement des messages…" @@ -1228,7 +1238,7 @@ msgid "Loading profile…" msgstr "Chargement du profil…" #: src/components/category/CategoryManager.tsx:60 -#: src/components/chat/ChatModal.tsx:762 +#: src/components/chat/ChatModal.tsx:763 #: src/components/editor/EmojiPickerPopup.tsx:125 #: src/components/playlist/PlaylistMembershipPanel.tsx:45 #: src/components/shared/ListState.tsx:38 @@ -1293,7 +1303,7 @@ msgstr "Couper le son" msgid "Name" msgstr "Nom" -#: src/components/chat/ChatModal.tsx:691 +#: src/components/chat/ChatModal.tsx:692 #: src/components/feed/FeedTabBar.tsx:40 #: src/pages/Settings.tsx:52 msgid "New" @@ -1349,7 +1359,7 @@ msgstr "Pas encore de collections suivies." msgid "No invitees yet." msgstr "Aucun invité pour le moment." -#: src/components/chat/ChatModal.tsx:675 +#: src/components/chat/ChatModal.tsx:676 msgid "No messages yet. Say hello!" msgstr "Aucun message pour l'instant. Dites bonjour !" @@ -1366,7 +1376,7 @@ msgstr "Aucune collection ne correspond à « {q} »." msgid "No playlists yet." msgstr "Pas encore de collections." -#: src/contexts/ws/WSProvider.tsx:929 +#: src/contexts/ws/WSProvider.tsx:932 msgid "No reply from the server — this message may not have been sent" msgstr "Pas de réponse du serveur — ce message n'a peut-être pas été envoyé" @@ -1388,13 +1398,13 @@ msgstr "Rien de neuf cette semaine." #: src/components/profile/ProfileDumpsTab.tsx:53 #: src/components/profile/ProfileOverviewTab.tsx:47 -#: src/pages/Notifications.tsx:513 +#: src/pages/Notifications.tsx:522 msgid "Nothing here yet." msgstr "Rien ici pour l'instant." #: src/components/layout/NotificationBell.tsx:57 -#: src/pages/Notifications.tsx:378 -#: src/pages/Notifications.tsx:496 +#: src/pages/Notifications.tsx:387 +#: src/pages/Notifications.tsx:505 #: src/pages/Settings.tsx:128 msgid "Notifications" msgstr "Notifications" @@ -1411,7 +1421,7 @@ msgstr "Désactivés" msgid "On" msgstr "Activés" -#: src/components/editor/TextEditor.tsx:385 +#: src/components/editor/TextEditor.tsx:391 msgid "Only images can be dropped here." msgstr "Seules les images peuvent être déposées ici." @@ -1594,13 +1604,13 @@ msgstr "Renommage…" msgid "Replace file" msgstr "Remplacer le fichier" -#: src/components/chat/ChatMessageItem.tsx:94 -#: src/components/chat/ChatMessageItem.tsx:95 +#: src/components/chat/ChatMessageItem.tsx:97 +#: src/components/chat/ChatMessageItem.tsx:98 #: src/components/comment/CommentNode.tsx:244 msgid "Reply" msgstr "Répondre" -#: src/components/chat/ChatModal.tsx:826 +#: src/components/chat/ChatModal.tsx:827 msgid "Replying to" msgstr "En réponse à" @@ -1687,7 +1697,7 @@ msgstr "Recherche…" msgid "Seek" msgstr "Position" -#: src/components/chat/ChatModal.tsx:866 +#: src/components/chat/ChatModal.tsx:868 msgid "Send" msgstr "Envoyer" @@ -1715,7 +1725,7 @@ msgstr "Définir un nouveau mot de passe" msgid "Settings" msgstr "Paramètres" -#: src/components/chat/ChatModal.tsx:650 +#: src/components/chat/ChatModal.tsx:651 msgid "Several people are typing…" msgstr "Plusieurs personnes écrivent…" @@ -1727,13 +1737,13 @@ msgstr "Usage unique, valable 7 jours." msgid "slug" msgstr "identifiant" -#: src/components/editor/TextEditor.tsx:343 +#: src/components/editor/TextEditor.tsx:349 msgid "Some images couldn't be uploaded." msgstr "Certaines images n'ont pas pu être envoyées." #: src/components/form/FormError.tsx:31 #: src/components/layout/PageError.tsx:29 -#: src/contexts/ws/WSProvider.tsx:729 +#: src/contexts/ws/WSProvider.tsx:732 #: src/pages/PlaylistDetail.tsx:124 msgid "Something went wrong" msgstr "Une erreur est survenue" @@ -1766,7 +1776,7 @@ msgstr "la tribune" msgid "The dump couldn't be removed. Try again." msgstr "Le dump n'a pas pu être retiré. Réessayez." -#: src/components/editor/TextEditor.tsx:344 +#: src/components/editor/TextEditor.tsx:350 msgid "The image couldn't be uploaded." msgstr "L'image n'a pas pu être envoyée." @@ -1825,7 +1835,7 @@ msgstr "Titre" msgid "Title is required." msgstr "Un titre est requis." -#: src/pages/Notifications.tsx:303 +#: src/pages/Notifications.tsx:312 #: src/utils/chat/time.ts:24 msgid "Today" msgstr "Aujourd'hui" @@ -1835,7 +1845,7 @@ msgstr "Aujourd'hui" msgid "Track {0}" msgstr "Piste {0}" -#: src/components/chat/ChatModal.tsx:667 +#: src/components/chat/ChatModal.tsx:668 msgid "Try again" msgstr "Réessayer" @@ -1851,7 +1861,7 @@ msgstr "Ne plus recevoir le résumé hebdomadaire ?" msgid "Turning off…" msgstr "Désactivation…" -#: src/components/chat/ChatModal.tsx:851 +#: src/components/chat/ChatModal.tsx:852 msgid "Type a message…" msgstr "Écrivez un message…" @@ -1892,7 +1902,7 @@ msgstr "Envoi échoué" msgid "Uploading {0}%" msgstr "Envoi {0} %" -#: src/components/editor/TextEditor.tsx:595 +#: src/components/editor/TextEditor.tsx:601 msgid "Uploading image…" msgstr "Envoi de l'image…" @@ -1990,7 +2000,7 @@ msgstr "Pourquoi ?" msgid "Write a reply…" msgstr "Écrire une réponse…" -#: src/pages/Notifications.tsx:304 +#: src/pages/Notifications.tsx:313 #: src/utils/chat/time.ts:27 msgid "Yesterday" msgstr "Hier" @@ -2007,11 +2017,11 @@ msgstr "Vous ne recevrez plus le récap mensuel. Vous pouvez le réactiver dans msgid "You won't get the weekly digest any more. You can turn it back on in your settings." msgstr "Vous ne recevrez plus le résumé hebdomadaire. Vous pouvez le réactiver dans vos paramètres." -#: src/pages/Notifications.tsx:516 +#: src/pages/Notifications.tsx:525 msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu." -#: src/contexts/ws/WSProvider.tsx:910 +#: src/contexts/ws/WSProvider.tsx:913 msgid "You're offline — not sent" msgstr "Hors ligne — message non envoyé" diff --git a/shared/chat.ts b/shared/chat.ts index 9c0dc19d..7200c5c5 100644 --- a/shared/chat.ts +++ b/shared/chat.ts @@ -28,4 +28,11 @@ export interface ChatMessage { replyToAuthor?: string; /** The first 300 characters of the replied-to body, in Markdown. */ replyToBody?: string; + /** + * Set when the body says `@everyone` and its author's role had + * `chat:everyone` when it was last written; absent otherwise. The client + * highlights the message for everyone and shows `@everyone` as a mention by + * it, never by the text, which anyone may type. + */ + mentionsEveryone?: boolean; } diff --git a/shared/mention.ts b/shared/mention.ts index b8152197..5d87a1b3 100644 --- a/shared/mention.ts +++ b/shared/mention.ts @@ -31,3 +31,20 @@ export function mentionedUsernames(body: string): string[] { ...new Set([...prose.matchAll(MENTION_RE)].map((m) => m[1].toLowerCase())), ]; } + +/** + * The name `@everyone` reaches. Said by a role with `chat:everyone` in the + * chat, it notifies every user, so no account may take it + * (`assertUsernameAvailable`). + */ +export const EVERYONE = "everyone"; + +/** + * Checks whether `body` says `@everyone`, in any case, by the rule of + * {@link mentionedUsernames}. Only the chat service reads it, to set a + * message's `mentionsEveryone` when its author may; the client reads that + * flag, not the text. + */ +export function mentionsEveryone(body: string): boolean { + return mentionedUsernames(body).includes(EVERYONE); +} diff --git a/shared/notification.ts b/shared/notification.ts index c93a2f17..38033f5e 100644 --- a/shared/notification.ts +++ b/shared/notification.ts @@ -86,7 +86,8 @@ export interface DumpUpvotedData extends DumpRef { /** * Carried by a `user_mentioned` notification, sent to a user @mentioned in a - * comment, a dump, a playlist's description or a chat message. Its + * comment, a dump, a playlist's description or a chat message, or to every + * user by a chat message's `@everyone`. Its * {@link DumpRef} is the dump's, or the comment's dump's; its * {@link PlaylistRef} the playlist's. */ @@ -106,6 +107,11 @@ export interface UserMentionedData extends DumpRef, PlaylistRef { * notification's link opens. */ dumpId?: string; + /** + * Set when the user was reached by `@everyone` rather than by name. A + * message naming them as well notifies them once, by name. + */ + everyone?: true; } /** Carried by a `dump_commented` notification, sent to the dump's poster. */ diff --git a/shared/permissions.ts b/shared/permissions.ts index 1fb6eec5..19a5cafb 100644 --- a/shared/permissions.ts +++ b/shared/permissions.ts @@ -18,6 +18,7 @@ export type Permission = | "comment:moderate" | "playlist:moderate" | "chat:moderate" + | "chat:everyone" | "user:manage" | "category:manage"; @@ -26,13 +27,15 @@ const ALL_PERMISSIONS: readonly Permission[] = [ "comment:moderate", "playlist:moderate", "chat:moderate", + "chat:everyone", "user:manage", "category:manage", ]; /** * Read through {@link hasPermission}. Roles are fixed in code. An admin holds - * every permission, and a moderator the moderation ones only. + * every permission, and a moderator the moderation ones and `@everyone` in + * the chat. */ export const ROLE_PERMISSIONS: Record = { admin: ALL_PERMISSIONS, @@ -41,6 +44,7 @@ export const ROLE_PERMISSIONS: Record = { "comment:moderate", "playlist:moderate", "chat:moderate", + "chat:everyone", ], user: [], }; diff --git a/src/App.css b/src/App.css index 8cb226db..938e0bcd 100644 --- a/src/App.css +++ b/src/App.css @@ -13,6 +13,12 @@ .md a:hover { text-decoration: none; } +/* A chat message's @everyone: a mention, in the accent, but no link. */ +.md .md-everyone { + color: var(--color-accent); + font-style: normal; + font-weight: 700; +} .md strong { font-weight: 700; } @@ -6909,6 +6915,13 @@ body.has-player .chat-fab { font-size: var(--text-md); color: var(--color-text); } +/* Stands where an avatar would, for @everyone. */ +.mention-dropdown-everyone { + width: 20px; + text-align: center; + font-size: 14px; + line-height: 20px; +} /* ── Emoji picker (frimousse) ── */ /* Portaled to and positioned from JS (see useAnchoredFloat): anchored diff --git a/src/components/chat/ChatMessageItem.tsx b/src/components/chat/ChatMessageItem.tsx index af2620e1..10837a2d 100644 --- a/src/components/chat/ChatMessageItem.tsx +++ b/src/components/chat/ChatMessageItem.tsx @@ -35,7 +35,10 @@ export interface ChatMessageItemProps { * float over its top. */ grouped: boolean; - /** Highlights a message that @mentions the user or replies to them. */ + /** + * Highlights a message that @mentions the user, `@everyone` included, or + * replies to them. + */ mentionsMe: boolean; /** * Set when the replied-to message was deleted live, after the server filled @@ -199,7 +202,10 @@ export function ChatMessageItem( /> ) : ( - + {message.body} )} diff --git a/src/components/chat/ChatModal.tsx b/src/components/chat/ChatModal.tsx index 0fa67c0e..1f9b63e5 100644 --- a/src/components/chat/ChatModal.tsx +++ b/src/components/chat/ChatModal.tsx @@ -143,6 +143,7 @@ export function ChatModal( } = useWSChat(); const { authFetch, user } = useAuth(); const canModerate = can(user, "chat:moderate"); + const canMentionEveryone = can(user, "chat:everyone"); const userId = user?.id; const [history, setHistory] = useState([]); @@ -701,7 +702,7 @@ export function ChatModal( message={m} grouped={row.grouped} mentionsMe={!isOwn && !!username && - (mentionsUser(m.body, username) || + (!!m.mentionsEveryone || mentionsUser(m.body, username) || m.replyToAuthor === username)} replyDeleted={!!m.replyToId && deletedIds.has(m.replyToId)} canEdit={isOwn || canModerate} @@ -853,6 +854,7 @@ export function ChatModal( autoResize maxLength={VALIDATION.CHAT_MESSAGE_MAX} countFrom={COUNT_FROM} + mentionEveryone={canMentionEveryone} className="comment-reply-textarea chat-input" onSubmit={handleSend} onKeyDown={handleComposerKeyDown} diff --git a/src/components/editor/Markdown.tsx b/src/components/editor/Markdown.tsx index 39d3af50..30f1edda 100644 --- a/src/components/editor/Markdown.tsx +++ b/src/components/editor/Markdown.tsx @@ -11,6 +11,7 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { useAuth } from "../../hooks/auth/useAuth.ts"; import { userPath } from "../../../shared/paths.ts"; +import { EVERYONE } from "../../../shared/mention.ts"; /** Passed to {@link Markdown} by any component that shows a user's text. */ export interface MarkdownProps { @@ -21,6 +22,12 @@ export interface MarkdownProps { * would break the layout. The full text is one click away. */ inline?: boolean; + /** + * Shows `@everyone` as a mention, from a chat message's + * `mentionsEveryone`. Without it, `@everyone` is plain text: anyone may type + * it, and it reaches no one. + */ + everyone?: boolean; } /** @@ -36,14 +43,18 @@ interface MdNode { value?: string; url?: string; children?: MdNode[]; + /** Read by mdast-util-to-hast: the element and attributes to render. */ + data?: { hName: string; hProperties: Record }; } /** * Turns a bare `@username` into a link to that user's profile. It works on the * parsed tree rather than the source, so code keeps its `@`s and a URL that * contains one (`https://mastodon.social/@bob`) isn't split in two. + * `@everyone` names no profile: it is left as text, or with `everyone` set, + * becomes a `span.md-everyone`. */ -function remarkMentions() { +function remarkMentions({ everyone = false }: { everyone?: boolean } = {}) { const walk = (node: MdNode) => { if (!node.children) return; // Link text is already a link, whatever it says. @@ -56,14 +67,27 @@ function remarkMentions() { const out: MdNode[] = []; let last = 0; for (const m of child.value.matchAll(MENTION_RE)) { + const isEveryone = m[1].toLowerCase() === EVERYONE; + if (isEveryone && !everyone) continue; if (m.index > last) { out.push({ type: "text", value: child.value.slice(last, m.index) }); } - out.push({ - type: "link", - url: userPath(m[1]), - children: [{ type: "text", value: m[0] }], - }); + out.push( + isEveryone + ? { + type: "emphasis", + data: { + hName: "span", + hProperties: { className: "md-everyone" }, + }, + children: [{ type: "text", value: m[0] }], + } + : { + type: "link", + url: userPath(m[1]), + children: [{ type: "text", value: m[0] }], + }, + ); last = m.index + m[0].length; } if (last === 0) return [child]; @@ -101,6 +125,13 @@ function remarkBreaks() { const REMARK_PLUGINS = [remarkGfm, remarkMentions, remarkBreaks]; +/** {@link REMARK_PLUGINS} for a text whose `@everyone` is a mention. */ +const REMARK_PLUGINS_EVERYONE = [ + remarkGfm, + [remarkMentions, { everyone: true }], + remarkBreaks, +] as React.ComponentProps["remarkPlugins"]; + /** * Returns the path of a link into this app, relative or absolute to this * origin, or null for anywhere else. The API's files and attachments count as @@ -180,7 +211,7 @@ const INLINE_MARKDOWN_COMPONENTS: Components = { export const Markdown: React.MemoExoticComponent< (props: MarkdownProps) => React.JSX.Element > = memo(function Markdown( - { children, className, inline = false }: MarkdownProps, + { children, className, inline = false, everyone = false }: MarkdownProps, ) { return (
{children} diff --git a/src/components/editor/MentionDropdown.tsx b/src/components/editor/MentionDropdown.tsx index 3d258602..3b7e3b47 100644 --- a/src/components/editor/MentionDropdown.tsx +++ b/src/components/editor/MentionDropdown.tsx @@ -8,6 +8,7 @@ import { type CSSProperties, useLayoutEffect, useRef } from "react"; import { Avatar } from "../shared/Avatar.tsx"; import type { UserSuggestion } from "../../../shared/user.ts"; +import { EVERYONE } from "../../../shared/mention.ts"; /** * Passed to {@link MentionDropdown} by `TextEditor`, from the state of @@ -65,12 +66,20 @@ export function MentionDropdown( onSelect(user.username); }} > - + {user.username === EVERYONE + ? ( + + ) + : ( + + )} @{user.username} ))} diff --git a/src/components/editor/TextEditor.tsx b/src/components/editor/TextEditor.tsx index 688871f3..4ae474fa 100644 --- a/src/components/editor/TextEditor.tsx +++ b/src/components/editor/TextEditor.tsx @@ -80,6 +80,11 @@ export interface TextEditorProps { * before the warning colour does. */ countFrom?: number; + /** + * Offers `@everyone` among the mention suggestions. The chat's composer + * sets it for a role with `chat:everyone`, the one place it notifies. + */ + mentionEveryone?: boolean; "aria-invalid"?: boolean; /** Joined with the id of the editor's own upload notice when it shows one. */ "aria-describedby"?: string; @@ -138,6 +143,7 @@ export const TextEditor: React.ForwardRefExoticComponent< onKeyDown, maxLength, countFrom, + mentionEveryone = false, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy, }, @@ -224,7 +230,7 @@ export const TextEditor: React.ForwardRefExoticComponent< closeMention, handleMentionKeyDown, handleMentionSelect, - } = useMentionAutocomplete(replace); + } = useMentionAutocomplete(replace, mentionEveryone); const { emojiMode, diff --git a/src/contexts/ws/WSProvider.tsx b/src/contexts/ws/WSProvider.tsx index 4e93964b..d8474a44 100644 --- a/src/contexts/ws/WSProvider.tsx +++ b/src/contexts/ws/WSProvider.tsx @@ -671,7 +671,10 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element { if (message.userId !== userIdRef.current) { if (!isViewingChat()) setUnreadChatCount((prev) => prev + 1); const username = usernameRef.current; - if (username && mentionsUser(message.body, username)) { + if ( + message.mentionsEveryone || + (username && mentionsUser(message.body, username)) + ) { // The server sends no notification for a mention read live, so // the chime comes from here. Otherwise the notification that // follows chimes. diff --git a/src/hooks/editor/useMentionAutocomplete.ts b/src/hooks/editor/useMentionAutocomplete.ts index db17a131..174cc43e 100644 --- a/src/hooks/editor/useMentionAutocomplete.ts +++ b/src/hooks/editor/useMentionAutocomplete.ts @@ -16,7 +16,7 @@ import { import { API_URL } from "../../config/api.ts"; import { insideCode } from "../../utils/editor/text.ts"; import type { UserSuggestion } from "../../../shared/user.ts"; -import { MENTION_RE } from "../../../shared/mention.ts"; +import { EVERYONE, MENTION_RE } from "../../../shared/mention.ts"; interface MentionState { open: boolean; @@ -37,6 +37,16 @@ const CLOSED: MentionState = { selectedIndex: 0, }; +/** + * Offered first, to those who may say it, when the query starts `everyone`. + * No user has the name, so `MentionDropdown` tells it apart by it. + */ +const EVERYONE_SUGGESTION: UserSuggestion = { + id: EVERYONE, + username: EVERYONE, + avatarMime: null, +}; + /** * Matches the `@query` ending at the caret by the rule of shared/mention.ts, * which the server notifies by, so that what is offered would notify. @@ -100,10 +110,12 @@ export interface UseMentionAutocompleteResult { * Returns the suggestions' state and the functions that drive them. A pick * goes through `replace`, `TextEditor`'s function that puts the text over * `[start, end)` of the textarea's value, followed by a space when `word` is - * set. + * set. With `everyone`, which the chat's composer sets for a role with + * `chat:everyone`, `@everyone` is offered too. */ export function useMentionAutocomplete( replace: (start: number, end: number, text: string, word: boolean) => void, + everyone = false, ): UseMentionAutocompleteResult { const [state, setStateRaw] = useState(CLOSED); // Mirrored synchronously, so that the key handler and a late response both @@ -165,13 +177,15 @@ export function useMentionAutocomplete( const current = stateRef.current; // Only the search for what is typed now may open or close the list. if (current.query !== mention.query) return; - if (body.success && body.data.length > 0) { - setState({ - ...current, - open: true, - results: body.data, - selectedIndex: 0, - }); + const results: UserSuggestion[] = [ + ...(everyone && + EVERYONE.startsWith(mention.query.toLowerCase()) + ? [EVERYONE_SUGGESTION] + : []), + ...(body.success ? body.data : []), + ]; + if (results.length > 0) { + setState({ ...current, open: true, results, selectedIndex: 0 }); } else { setState({ ...current, open: false, results: [] }); } @@ -180,7 +194,7 @@ export function useMentionAutocomplete( } }, 150); }, - [cancelLookup, close, setState], + [cancelLookup, close, everyone, setState], ); const select = useCallback( diff --git a/src/pages/Notifications.tsx b/src/pages/Notifications.tsx index e2285ec2..2d53e4f0 100644 --- a/src/pages/Notifications.tsx +++ b/src/pages/Notifications.tsx @@ -261,6 +261,15 @@ function notificationContent(n: Notification): React.ReactNode { : d.contextType === "chat" ? t`the chat` : t`a post`); + if (d.everyone) { + return ( + + {d.mentionerUsername} + {" mentioned everyone in "} + {where} + + ); + } return ( {d.mentionerUsername} diff --git a/src/test/components/chat/ChatModal.test.tsx b/src/test/components/chat/ChatModal.test.tsx index 6c1eaa2a..7852ee88 100644 --- a/src/test/components/chat/ChatModal.test.tsx +++ b/src/test/components/chat/ChatModal.test.tsx @@ -421,6 +421,33 @@ describe("ChatModal: live", () => { ); }); + test("@everyone stands out only when the server flagged it", async () => { + const { socket } = await setup(); + for ( + const [id, body, mentionsEveryone] of [ + ["e1", "@everyone typed by anyone", undefined], + ["e2", "@everyone from a moderator", true], + ] as const + ) { + socket.receive({ + type: "chat_message", + message: wire( + makeChatMessage({ + id, + body, + createdAt: new Date(), + mentionsEveryone, + }), + ), + }); + } + const row = (text: RegExp) => + screen.getByText(text).closest(".chat-message"); + expect(row(/typed by anyone/)).not.toHaveClass("chat-message--mention"); + expect(row(/from a moderator/)).toHaveClass("chat-message--mention"); + expect(screen.getByText("@everyone")).toHaveClass("md-everyone"); + }); + test("an emoji-only message is shown large", async () => { await setup([makeChatMessage({ body: "🎉🎉" })]); expect(document.querySelector(".chat-message-text--jumbo")) diff --git a/src/test/components/editor/Markdown.test.tsx b/src/test/components/editor/Markdown.test.tsx index 53fc512f..96367b5a 100644 --- a/src/test/components/editor/Markdown.test.tsx +++ b/src/test/components/editor/Markdown.test.tsx @@ -33,6 +33,18 @@ describe("Markdown", () => { expect(currentPath()).toBe("/@ann"); }); + test("@everyone links nowhere, and shows as a mention only when flagged", () => { + md("hi @everyone"); + expect(screen.queryByRole("link")).toBeNull(); + expect(screen.getByText("hi @everyone")).toBeInTheDocument(); + renderWithContexts({"yo @Everyone and @ann"}); + expect(screen.getByText("@Everyone")).toHaveClass("md-everyone"); + expect(screen.getByRole("link", { name: "@ann" })).toHaveAttribute( + "href", + "/@ann", + ); + }); + test("mentions in code and inside URLs are left alone", () => { md("`@ann` and https://mastodon.social/@bob"); expect(screen.getByText("@ann").tagName).toBe("CODE"); diff --git a/src/test/hooks/editor/useMentionAutocomplete.test.tsx b/src/test/hooks/editor/useMentionAutocomplete.test.tsx index a64e167f..b7fa4c9a 100644 --- a/src/test/hooks/editor/useMentionAutocomplete.test.tsx +++ b/src/test/hooks/editor/useMentionAutocomplete.test.tsx @@ -29,9 +29,9 @@ describe("useMentionAutocomplete", () => { { id: "3", username: "albert", avatarMime: null }, ]; - function setup() { + function setup(everyone = false) { const replace = vi.fn(); - const hook = renderHook(() => useMentionAutocomplete(replace)); + const hook = renderHook(() => useMentionAutocomplete(replace, everyone)); const type = async (value: string, cursor = value.length) => { act(() => hook.result.current.syncMention(value, cursor, true)); await act(() => vi.advanceTimersByTimeAsync(200)); @@ -56,6 +56,21 @@ describe("useMentionAutocomplete", () => { ); }); + test("offers @everyone first, only when allowed", async () => { + route("GET /api/users", () => [users[0]]); + const allowed = setup(true); + await allowed.type("@E"); + expect(allowed.hook.result.current.mentionResults.map((u) => u.username)) + .toEqual(["everyone", "alice"]); + await allowed.type("@al"); + expect(allowed.hook.result.current.mentionResults.map((u) => u.username)) + .toEqual(["alice"]); + const denied = setup(); + await denied.type("@e"); + expect(denied.hook.result.current.mentionResults.map((u) => u.username)) + .toEqual(["alice"]); + }); + test("debounces keystrokes into one lookup", async () => { route("GET /api/users", () => users); const { hook } = setup(); diff --git a/src/test/pages/Notifications.test.tsx b/src/test/pages/Notifications.test.tsx index 2332a812..850f5e86 100644 --- a/src/test/pages/Notifications.test.tsx +++ b/src/test/pages/Notifications.test.tsx @@ -134,6 +134,17 @@ const n = { contextTitle: "", }, }), + mentionEveryone: makeNotification({ + type: "user_mentioned", + data: { + mentionerId: "m", + mentionerUsername: "mo", + contextType: "chat", + contextId: "y", + contextTitle: "", + everyone: true, + }, + }), mentionPlaylist: makeNotification({ type: "user_mentioned", data: { @@ -170,8 +181,9 @@ describe("Notifications", () => { "/notifications?chat=x", "/@bob/playlists/faves", ]); + expect(await link(/mentioned everyone in/)).toBe("/notifications?chat=y"); expect(screen.getByText("a comment")).toBeInTheDocument(); - expect(screen.getByText("the chat")).toBeInTheDocument(); + expect(screen.getAllByText("the chat")).toHaveLength(2); expect(screen.getByText("Faves")).toBeInTheDocument(); });