v3: chat mentions stop going missing, and live events get a light sound — the server skips a mention notification for anyone with the chatbox open, on the grounds that they're reading it, but "open" also meant a chatbox left open in a background tab, and the same modal cleared the unread badge on every arrival, so a mention there left no bell, no badge and no trace. Reading the chat now means open and on screen: the focus flag is re-announced on every visibilitychange, and the unread count only clears while the tab is visible. A reconnect with the chatbox open refetches the latest page, since whatever was said while the socket was down never reached the live buffer. The server's mention regex excluded a backslash and the letter w rather than word characters (\\w inside a class), so an email address mentioned its domain; it now matches the autocomplete's rule. On top of that, sounds synthesised with Web Audio — a soft blip for chat you aren't looking at, throttled, and a two-note chime for notifications and for a mention you're reading live, which gets no notification of its own — claimed through localStorage so several tabs chime once, with an on/off toggle under a new Notifications section in settings. The tab title carries the unread count, composed with the page title in useDocumentTitle so navigation doesn't drop it.
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 46s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 46s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,8 @@ import { db, isNotificationRow, notificationRowToApi } from "../model/db.ts";
|
||||
import { sendToUser } from "./ws-service.ts";
|
||||
|
||||
// Regex: matches @username not already inside a markdown link ([...] or (...)
|
||||
const MENTION_RE = /(?<![[(\\w])@([\w]+)/g;
|
||||
// and not mid-word, so an email address doesn't mention its domain.
|
||||
const MENTION_RE = /(?<![[(\w])@(\w+)/g;
|
||||
|
||||
// ── Core CRUD ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ export function ChatModal({ onClose }: { onClose: () => void }) {
|
||||
clearUnreadChat,
|
||||
setChatTyping,
|
||||
setChatOpen,
|
||||
connectionEpoch,
|
||||
} = useWS();
|
||||
const { authFetch, user } = useAuth();
|
||||
const canModerate = can(user, "chat:moderate");
|
||||
@@ -352,10 +353,43 @@ export function ChatModal({ onClose }: { onClose: () => void }) {
|
||||
};
|
||||
}, [authFetch]);
|
||||
|
||||
// Clear the unread badge on open and as new messages arrive while open.
|
||||
// Messages sent while the socket was down never reached the live buffer:
|
||||
// after a reconnect, fetch the latest page again and fold it into history
|
||||
// (the merge above dedupes against what's already shown).
|
||||
const seenEpochRef = useRef(connectionEpoch);
|
||||
useEffect(() => {
|
||||
clearUnreadChat();
|
||||
}, [chatMessages, clearUnreadChat]);
|
||||
if (connectionEpoch === seenEpochRef.current) return;
|
||||
seenEpochRef.current = connectionEpoch;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await authFetch(`${API_URL}/api/chat?limit=${PAGE_SIZE}`);
|
||||
const json = await res.json();
|
||||
if (cancelled || !json.success) return;
|
||||
const items = (json.data as RawChatMessage[]).map(deserializeChatMessage);
|
||||
setHistory((prev) => [...prev, ...items]);
|
||||
} catch {
|
||||
// The next reconnect or reopen will try again.
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [authFetch, connectionEpoch]);
|
||||
|
||||
// Whether the tab is on screen: a chatbox left open in a background tab isn't
|
||||
// being read, so its messages should stay unread until the user comes back.
|
||||
const [tabVisible, setTabVisible] = useState(() => !document.hidden);
|
||||
useEffect(() => {
|
||||
const onVisibility = () => setTabVisible(!document.hidden);
|
||||
document.addEventListener("visibilitychange", onVisibility);
|
||||
return () => document.removeEventListener("visibilitychange", onVisibility);
|
||||
}, []);
|
||||
|
||||
// Clear the unread badge on open and as new messages arrive while read.
|
||||
useEffect(() => {
|
||||
if (tabVisible) clearUnreadChat();
|
||||
}, [chatMessages, clearUnreadChat, tabVisible]);
|
||||
|
||||
// Tell the server the chatbox is open so it skips redundant mention
|
||||
// notifications while the user is reading; clear the flag on close.
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
} from "../model.ts";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { setTitleBadge } from "../hooks/useDocumentTitle.ts";
|
||||
import { playChime } from "../utils/sound.ts";
|
||||
|
||||
interface WSProviderProps {
|
||||
children: ReactNode;
|
||||
@@ -55,6 +57,17 @@ const MAX_RECENT_DUMPS = 100;
|
||||
// Older messages remain reachable via the /api/chat history endpoint.
|
||||
const MAX_CHAT_MESSAGES = 200;
|
||||
|
||||
// Mirrors MENTION_RE in api/services/notification-service.ts.
|
||||
const MENTION_RE = /(?<![[(\w])@(\w+)/g;
|
||||
|
||||
function mentionsUser(body: string, username: string): boolean {
|
||||
const target = username.toLowerCase();
|
||||
for (const m of body.matchAll(MENTION_RE)) {
|
||||
if (m[1].toLowerCase() === target) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface PendingVote {
|
||||
timeout: ReturnType<typeof setTimeout>;
|
||||
rollback: () => void;
|
||||
@@ -167,6 +180,7 @@ export function WSProvider({ children }: WSProviderProps) {
|
||||
const likeCountsRef = useRef(likeCounts);
|
||||
const myLikesRef = useRef(myLikes);
|
||||
const userIdRef = useRef(userId);
|
||||
const usernameRef = useRef(user?.username ?? null);
|
||||
// Stable ref for logout so the effect doesn't reconnect when the function
|
||||
// reference changes on re-renders.
|
||||
const onForceLogoutRef = useRef(logout);
|
||||
@@ -176,6 +190,7 @@ export function WSProvider({ children }: WSProviderProps) {
|
||||
likeCountsRef.current = likeCounts;
|
||||
myLikesRef.current = myLikes;
|
||||
userIdRef.current = userId;
|
||||
usernameRef.current = user?.username ?? null;
|
||||
onForceLogoutRef.current = logout;
|
||||
});
|
||||
|
||||
@@ -183,6 +198,13 @@ export function WSProvider({ children }: WSProviderProps) {
|
||||
// Tracks whether the chatbox is open so it can be re-announced to the server
|
||||
// after a reconnect (the fresh socket starts with the flag cleared).
|
||||
const chatOpenRef = useRef(false);
|
||||
// The user is only actually reading the chat when the chatbox is open *and*
|
||||
// the tab is on screen: a chatbox left open in a background tab must not
|
||||
// swallow mention notifications or unread counts.
|
||||
const isViewingChat = useCallback(
|
||||
() => chatOpenRef.current && !document.hidden,
|
||||
[],
|
||||
);
|
||||
// Tracks pending optimistic votes: dumpId → pending rollback handler
|
||||
const pendingRef = useRef<Map<string, PendingVote>>(
|
||||
new Map(),
|
||||
@@ -309,7 +331,7 @@ export function WSProvider({ children }: WSProviderProps) {
|
||||
if (welcomeCount > 1) setConnectionEpoch((n) => n + 1);
|
||||
// Restore the chat-open flag on the fresh connection so mention
|
||||
// suppression keeps working across reconnects.
|
||||
if (chatOpenRef.current) {
|
||||
if (isViewingChat()) {
|
||||
ws.send(
|
||||
JSON.stringify(
|
||||
{ type: "chat_focus", open: true } satisfies OutgoingWSMessage,
|
||||
@@ -496,6 +518,7 @@ export function WSProvider({ children }: WSProviderProps) {
|
||||
const notification = deserializeNotification(msg.notification);
|
||||
setLastNotification(notification);
|
||||
setUnreadNotificationCount((prev) => prev + 1);
|
||||
playChime("notify", notification.id);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -508,6 +531,15 @@ export function WSProvider({ children }: WSProviderProps) {
|
||||
// Count as unread unless it's the current user's own message.
|
||||
if (message.userId !== userIdRef.current) {
|
||||
setUnreadChatCount((prev) => prev + 1);
|
||||
const username = usernameRef.current;
|
||||
if (username && mentionsUser(message.body, username)) {
|
||||
// Someone reading the chat gets no notification for a mention
|
||||
// (the server skips it), so the chime has to come from here;
|
||||
// otherwise the notification that follows brings its own.
|
||||
if (isViewingChat()) playChime("notify", message.id);
|
||||
} else if (!isViewingChat()) {
|
||||
playChime("chat", message.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -610,6 +642,7 @@ export function WSProvider({ children }: WSProviderProps) {
|
||||
clearPendingVote,
|
||||
clearAllPendingLikes,
|
||||
clearPendingLike,
|
||||
isViewingChat,
|
||||
token,
|
||||
]);
|
||||
|
||||
@@ -817,16 +850,39 @@ export function WSProvider({ children }: WSProviderProps) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setChatOpen = useCallback((open: boolean) => {
|
||||
chatOpenRef.current = open;
|
||||
// Tell the server whether the user is reading the chat right now (chatbox
|
||||
// open and tab visible), so it knows when a mention notification is moot.
|
||||
const sendChatFocus = useCallback(() => {
|
||||
if (socketRef.current?.readyState === WebSocket.OPEN) {
|
||||
socketRef.current.send(
|
||||
JSON.stringify(
|
||||
{ type: "chat_focus", open } satisfies OutgoingWSMessage,
|
||||
{
|
||||
type: "chat_focus",
|
||||
open: isViewingChat(),
|
||||
} satisfies OutgoingWSMessage,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
}, [isViewingChat]);
|
||||
|
||||
const setChatOpen = useCallback((open: boolean) => {
|
||||
chatOpenRef.current = open;
|
||||
sendChatFocus();
|
||||
}, [sendChatFocus]);
|
||||
|
||||
// Re-announce chat focus whenever the tab is hidden or shown.
|
||||
useEffect(() => {
|
||||
const onVisibility = () => {
|
||||
if (chatOpenRef.current) sendChatFocus();
|
||||
};
|
||||
document.addEventListener("visibilitychange", onVisibility);
|
||||
return () => document.removeEventListener("visibilitychange", onVisibility);
|
||||
}, [sendChatFocus]);
|
||||
|
||||
// Unread count in the tab title, so a background tab shows there's news.
|
||||
useEffect(() => {
|
||||
setTitleBadge(unreadNotificationCount + unreadChatCount);
|
||||
}, [unreadNotificationCount, unreadChatCount]);
|
||||
|
||||
const value: WSContextValue = useMemo(() => ({
|
||||
wsStatus,
|
||||
|
||||
@@ -23,6 +23,19 @@ function readSiteEmoji(): string {
|
||||
|
||||
export const SITE_EMOJI = readSiteEmoji();
|
||||
|
||||
// The page title and the unread badge are set from different places (each
|
||||
// page, and the live-updates provider), so both are kept here and composed.
|
||||
// `null` base means no page has set one yet: keep the server-rendered title.
|
||||
let baseTitle: string | null = null;
|
||||
let badge = 0;
|
||||
|
||||
function applyTitle() {
|
||||
const base = baseTitle ?? document.title.replace(/^\(\d+\+?\) /, "");
|
||||
document.title = badge > 0
|
||||
? `(${badge > 99 ? "99+" : badge}) ${base}`
|
||||
: base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps document.title in sync with the current page during client-side
|
||||
* navigation. Without this the tab title only ever reflects the server-rendered
|
||||
@@ -37,6 +50,14 @@ export const SITE_EMOJI = readSiteEmoji();
|
||||
export function useDocumentTitle(title: string | null | undefined): void {
|
||||
useEffect(() => {
|
||||
if (title === undefined) return;
|
||||
document.title = title ? `${title} · ${SITE_NAME}` : SITE_NAME;
|
||||
baseTitle = title ? `${title} · ${SITE_NAME}` : SITE_NAME;
|
||||
applyTitle();
|
||||
}, [title]);
|
||||
}
|
||||
|
||||
/** Prefixes the tab title with an unread count, e.g. "(3) Chat · gerbeur". */
|
||||
export function setTitleBadge(count: number): void {
|
||||
if (count === badge) return;
|
||||
badge = count;
|
||||
applyTitle();
|
||||
}
|
||||
|
||||
18
src/hooks/useSoundsEnabled.ts
Normal file
18
src/hooks/useSoundsEnabled.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { readSoundsEnabled, writeSoundsEnabled } from "../utils/sound.ts";
|
||||
|
||||
/**
|
||||
* User preference for notification and chat sounds, persisted to localStorage
|
||||
* (on by default). The chime itself reads the stored value at play time, so
|
||||
* this hook only drives the settings toggle.
|
||||
*/
|
||||
export function useSoundsEnabled(): [boolean, (enabled: boolean) => void] {
|
||||
const [enabled, setEnabledState] = useState(readSoundsEnabled);
|
||||
|
||||
const setEnabled = useCallback((next: boolean) => {
|
||||
writeSoundsEnabled(next);
|
||||
setEnabledState(next);
|
||||
}, []);
|
||||
|
||||
return [enabled, setEnabled];
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -35,12 +35,12 @@ msgstr "{0, plural, one {and # earlier dump} other {and # earlier dumps}}"
|
||||
|
||||
#. placeholder {0}: typingNames[0]
|
||||
#. placeholder {1}: typingNames[1]
|
||||
#: src/components/ChatModal.tsx:534
|
||||
#: src/components/ChatModal.tsx:568
|
||||
msgid "{0} and {1} are typing…"
|
||||
msgstr "{0} and {1} are typing…"
|
||||
|
||||
#. placeholder {0}: typingNames[0]
|
||||
#: src/components/ChatModal.tsx:532
|
||||
#: src/components/ChatModal.tsx:566
|
||||
msgid "{0} is typing…"
|
||||
msgstr "{0} is typing…"
|
||||
|
||||
@@ -60,7 +60,7 @@ msgstr "{visibleCount, plural, one {# comment} other {# comments}}"
|
||||
|
||||
#: src/components/DumpCreateModal.tsx:936
|
||||
#: src/pages/PlaylistDetail.tsx:572
|
||||
#: src/pages/UserPublicProfile.tsx:798
|
||||
#: src/pages/UserPublicProfile.tsx:801
|
||||
msgid "← Back"
|
||||
msgstr "← Back"
|
||||
|
||||
@@ -76,7 +76,7 @@ msgstr "← Back to all dumps"
|
||||
msgid "← Back to profile"
|
||||
msgstr "← Back to profile"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:134
|
||||
#: src/pages/UserPublicProfile.tsx:136
|
||||
msgid "+ Invite someone"
|
||||
msgstr "+ Invite someone"
|
||||
|
||||
@@ -142,7 +142,7 @@ msgstr "a comment"
|
||||
msgid "a post"
|
||||
msgstr "a post"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1179
|
||||
#: src/pages/UserPublicProfile.tsx:1182
|
||||
msgid "Account"
|
||||
msgstr "Account"
|
||||
|
||||
@@ -155,7 +155,7 @@ msgstr "Add a comment…"
|
||||
msgid "Add category"
|
||||
msgstr "Add category"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:876
|
||||
#: src/pages/UserPublicProfile.tsx:879
|
||||
msgid "Add email…"
|
||||
msgstr "Add email…"
|
||||
|
||||
@@ -164,7 +164,7 @@ msgstr "Add email…"
|
||||
msgid "Add to playlist"
|
||||
msgstr "Add to playlist"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1604
|
||||
#: src/pages/UserPublicProfile.tsx:1646
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
@@ -183,7 +183,7 @@ msgstr "Already dumped by {0} {1}"
|
||||
msgid "Already have an account? <0>Log in</0>"
|
||||
msgstr "Already have an account? <0>Log in</0>"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1198
|
||||
#: src/pages/UserPublicProfile.tsx:1201
|
||||
msgid "Appearance"
|
||||
msgstr "Appearance"
|
||||
|
||||
@@ -194,7 +194,7 @@ msgstr "Appearance"
|
||||
msgid "At least {0} characters"
|
||||
msgstr "At least {0} characters"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1253
|
||||
#: src/pages/UserPublicProfile.tsx:1263
|
||||
msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
@@ -203,8 +203,8 @@ msgstr "Auto"
|
||||
msgid "Back to login"
|
||||
msgstr "Back to login"
|
||||
|
||||
#: src/contexts/WSProvider.tsx:270
|
||||
#: src/contexts/WSProvider.tsx:576
|
||||
#: src/contexts/WSProvider.tsx:292
|
||||
#: src/contexts/WSProvider.tsx:608
|
||||
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."
|
||||
|
||||
@@ -218,8 +218,8 @@ msgstr "Can't connect to the live updates server. Upvotes and notifications may
|
||||
#: src/pages/Dump.tsx:406
|
||||
#: src/pages/DumpEdit.tsx:484
|
||||
#: src/pages/PlaylistDetail.tsx:923
|
||||
#: src/pages/UserPublicProfile.tsx:1733
|
||||
#: src/pages/UserPublicProfile.tsx:1806
|
||||
#: src/pages/UserPublicProfile.tsx:1775
|
||||
#: src/pages/UserPublicProfile.tsx:1848
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
@@ -227,8 +227,8 @@ msgstr "Cancel"
|
||||
msgid "Cancel removal"
|
||||
msgstr "Cancel removal"
|
||||
|
||||
#: src/components/ChatModal.tsx:624
|
||||
#: src/components/ChatModal.tsx:625
|
||||
#: src/components/ChatModal.tsx:658
|
||||
#: src/components/ChatModal.tsx:659
|
||||
msgid "Cancel reply"
|
||||
msgstr "Cancel reply"
|
||||
|
||||
@@ -237,7 +237,7 @@ msgid "Cancel search"
|
||||
msgstr "Cancel search"
|
||||
|
||||
#: src/components/CategorySelect.tsx:22
|
||||
#: src/pages/UserPublicProfile.tsx:1336
|
||||
#: src/pages/UserPublicProfile.tsx:1378
|
||||
msgid "Categories"
|
||||
msgstr "Categories"
|
||||
|
||||
@@ -245,7 +245,7 @@ msgstr "Categories"
|
||||
msgid "Category"
|
||||
msgstr "Category"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:825
|
||||
#: src/pages/UserPublicProfile.tsx:828
|
||||
msgid "Change avatar"
|
||||
msgstr "Change avatar"
|
||||
|
||||
@@ -254,14 +254,14 @@ msgstr "Change avatar"
|
||||
msgid "Change password"
|
||||
msgstr "Change password"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1191
|
||||
#: src/pages/UserPublicProfile.tsx:1194
|
||||
msgid "Change password…"
|
||||
msgstr "Change password…"
|
||||
|
||||
#: src/components/ChatButton.tsx:17
|
||||
#: src/components/ChatFab.tsx:49
|
||||
#: src/components/ChatFab.tsx:50
|
||||
#: src/components/ChatModal.tsx:538
|
||||
#: src/components/ChatModal.tsx:572
|
||||
msgid "Chat"
|
||||
msgstr "Chat"
|
||||
|
||||
@@ -295,7 +295,7 @@ msgstr "Close without posting? The attached file won't be kept — everything yo
|
||||
msgid "Collapse player"
|
||||
msgstr "Collapse player"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1245
|
||||
#: src/pages/UserPublicProfile.tsx:1255
|
||||
msgid "Color scheme"
|
||||
msgstr "Color scheme"
|
||||
|
||||
@@ -304,11 +304,11 @@ msgstr "Color scheme"
|
||||
msgid "Confirm new password"
|
||||
msgstr "Confirm new password"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:159
|
||||
#: src/pages/UserPublicProfile.tsx:161
|
||||
msgid "Copied!"
|
||||
msgstr "Copied!"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:159
|
||||
#: src/pages/UserPublicProfile.tsx:161
|
||||
msgid "Copy"
|
||||
msgstr "Copy"
|
||||
|
||||
@@ -350,11 +350,11 @@ msgstr "Creating…"
|
||||
msgid "Current password"
|
||||
msgstr "Current password"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1267
|
||||
#: src/pages/UserPublicProfile.tsx:1277
|
||||
msgid "Dark"
|
||||
msgstr "Dark"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1280
|
||||
#: src/pages/UserPublicProfile.tsx:1290
|
||||
msgid "Default tab"
|
||||
msgstr "Default tab"
|
||||
|
||||
@@ -446,13 +446,13 @@ msgstr "Dumped!"
|
||||
|
||||
#: src/pages/Search.tsx:174
|
||||
#: src/pages/UserDumps.tsx:80
|
||||
#: src/pages/UserPublicProfile.tsx:948
|
||||
#: src/pages/UserPublicProfile.tsx:951
|
||||
msgid "Dumps"
|
||||
msgstr "Dumps"
|
||||
|
||||
#. placeholder {0}: dumps.items.length
|
||||
#. placeholder {1}: dumps.hasMore ? "+" : ""
|
||||
#: src/pages/UserPublicProfile.tsx:968
|
||||
#: src/pages/UserPublicProfile.tsx:971
|
||||
msgid "Dumps ({0}{1})"
|
||||
msgstr "Dumps ({0}{1})"
|
||||
|
||||
@@ -530,9 +530,9 @@ msgstr "Failed to create playlist"
|
||||
msgid "Failed to delete"
|
||||
msgstr "Failed to delete"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:104
|
||||
#: src/pages/UserPublicProfile.tsx:107
|
||||
#: src/pages/UserPublicProfile.tsx:168
|
||||
#: src/pages/UserPublicProfile.tsx:106
|
||||
#: src/pages/UserPublicProfile.tsx:109
|
||||
#: src/pages/UserPublicProfile.tsx:170
|
||||
msgid "Failed to generate invite"
|
||||
msgstr "Failed to generate invite"
|
||||
|
||||
@@ -541,9 +541,9 @@ msgstr "Failed to generate invite"
|
||||
#: src/pages/index/JournalFeed.tsx:33
|
||||
#: src/pages/index/NewFeed.tsx:36
|
||||
#: src/pages/Notifications.tsx:374
|
||||
#: src/pages/UserPublicProfile.tsx:1070
|
||||
#: src/pages/UserPublicProfile.tsx:1112
|
||||
#: src/pages/UserPublicProfile.tsx:1157
|
||||
#: src/pages/UserPublicProfile.tsx:1073
|
||||
#: src/pages/UserPublicProfile.tsx:1115
|
||||
#: src/pages/UserPublicProfile.tsx:1160
|
||||
msgid "Failed to load"
|
||||
msgstr "Failed to load"
|
||||
|
||||
@@ -560,8 +560,8 @@ msgid "Failed to post reply"
|
||||
msgstr "Failed to post reply"
|
||||
|
||||
#: src/pages/PlaylistDetail.tsx:911
|
||||
#: src/pages/UserPublicProfile.tsx:1736
|
||||
#: src/pages/UserPublicProfile.tsx:1808
|
||||
#: src/pages/UserPublicProfile.tsx:1778
|
||||
#: src/pages/UserPublicProfile.tsx:1850
|
||||
msgid "Failed to save"
|
||||
msgstr "Failed to save"
|
||||
|
||||
@@ -573,15 +573,15 @@ msgstr "Failed to save category"
|
||||
msgid "Failed to save edit"
|
||||
msgstr "Failed to save edit"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:885
|
||||
#: src/pages/UserPublicProfile.tsx:888
|
||||
msgid "Failed to update avatar"
|
||||
msgstr "Failed to update avatar"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1674
|
||||
#: src/pages/UserPublicProfile.tsx:1716
|
||||
msgid "Failed to update role"
|
||||
msgstr "Failed to update role"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1275
|
||||
#: src/pages/UserPublicProfile.tsx:1285
|
||||
msgid "Feeds"
|
||||
msgstr "Feeds"
|
||||
|
||||
@@ -615,8 +615,8 @@ msgid "Follow some users to see their dumps here."
|
||||
msgstr "Follow some users to see their dumps here."
|
||||
|
||||
#: src/components/FeedTabBar.tsx:34
|
||||
#: src/pages/UserPublicProfile.tsx:950
|
||||
#: src/pages/UserPublicProfile.tsx:1309
|
||||
#: src/pages/UserPublicProfile.tsx:953
|
||||
#: src/pages/UserPublicProfile.tsx:1319
|
||||
msgid "Followed"
|
||||
msgstr "Followed"
|
||||
|
||||
@@ -626,13 +626,13 @@ msgstr "Followed"
|
||||
msgid "Followed ({0}{1})"
|
||||
msgstr "Followed ({0}{1})"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1101
|
||||
#: src/pages/UserPublicProfile.tsx:1104
|
||||
msgid "Followed playlists"
|
||||
msgstr "Followed playlists"
|
||||
|
||||
#: src/components/FollowButton.tsx:37
|
||||
#: src/components/FollowButton.tsx:64
|
||||
#: src/pages/UserPublicProfile.tsx:1059
|
||||
#: src/pages/UserPublicProfile.tsx:1062
|
||||
msgid "Following"
|
||||
msgstr "Following"
|
||||
|
||||
@@ -657,7 +657,7 @@ msgid "Go to login"
|
||||
msgstr "Go to login"
|
||||
|
||||
#: src/components/FeedTabBar.tsx:30
|
||||
#: src/pages/UserPublicProfile.tsx:1288
|
||||
#: src/pages/UserPublicProfile.tsx:1298
|
||||
msgid "Hot"
|
||||
msgstr "Hot"
|
||||
|
||||
@@ -677,22 +677,22 @@ msgstr "Invalid invite"
|
||||
msgid "Invalid link"
|
||||
msgstr "Invalid link"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:142
|
||||
#: src/pages/UserPublicProfile.tsx:149
|
||||
#: src/pages/UserPublicProfile.tsx:144
|
||||
#: src/pages/UserPublicProfile.tsx:151
|
||||
msgid "Invite link"
|
||||
msgstr "Invite link"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:847
|
||||
#: src/pages/UserPublicProfile.tsx:850
|
||||
msgid "invited by"
|
||||
msgstr "invited by"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:951
|
||||
#: src/pages/UserPublicProfile.tsx:1146
|
||||
#: src/pages/UserPublicProfile.tsx:954
|
||||
#: src/pages/UserPublicProfile.tsx:1149
|
||||
msgid "Invitees"
|
||||
msgstr "Invitees"
|
||||
|
||||
#: src/components/FeedTabBar.tsx:32
|
||||
#: src/pages/UserPublicProfile.tsx:1302
|
||||
#: src/pages/UserPublicProfile.tsx:1312
|
||||
msgid "Journal"
|
||||
msgstr "Journal"
|
||||
|
||||
@@ -700,7 +700,7 @@ msgstr "Journal"
|
||||
msgid "Keep editing"
|
||||
msgstr "Keep editing"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1260
|
||||
#: src/pages/UserPublicProfile.tsx:1270
|
||||
msgid "Light"
|
||||
msgstr "Light"
|
||||
|
||||
@@ -716,7 +716,7 @@ msgstr "Link"
|
||||
msgid "Link or file"
|
||||
msgstr "Link or file"
|
||||
|
||||
#: src/contexts/WSProvider.tsx:575
|
||||
#: src/contexts/WSProvider.tsx:607
|
||||
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||
msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||
|
||||
@@ -729,7 +729,7 @@ msgstr "Live updates unavailable."
|
||||
msgid "Load more"
|
||||
msgstr "Load more"
|
||||
|
||||
#: src/components/ChatModal.tsx:551
|
||||
#: src/components/ChatModal.tsx:585
|
||||
msgid "Load older messages"
|
||||
msgstr "Load older messages"
|
||||
|
||||
@@ -740,7 +740,7 @@ msgstr "Loading dump…"
|
||||
|
||||
#: src/pages/index/FollowedFeed.tsx:111
|
||||
#: src/pages/index/HotFeed.tsx:64
|
||||
#: src/pages/index/JournalFeed.tsx:62
|
||||
#: src/pages/index/JournalFeed.tsx:73
|
||||
#: src/pages/index/NewFeed.tsx:64
|
||||
#: src/pages/Search.tsx:244
|
||||
#: src/pages/UserDumps.tsx:121
|
||||
@@ -754,12 +754,12 @@ msgstr "Loading more…"
|
||||
msgid "Loading playlist…"
|
||||
msgstr "Loading playlist…"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:781
|
||||
#: src/pages/UserPublicProfile.tsx:784
|
||||
msgid "Loading profile…"
|
||||
msgstr "Loading profile…"
|
||||
|
||||
#: src/components/CategoryManager.tsx:52
|
||||
#: src/components/ChatModal.tsx:550
|
||||
#: src/components/ChatModal.tsx:584
|
||||
#: src/components/PlaylistMembershipPanel.tsx:28
|
||||
#: src/components/TextEditor.tsx:311
|
||||
#: src/components/UserListPopover.tsx:192
|
||||
@@ -772,9 +772,9 @@ msgstr "Loading profile…"
|
||||
#: src/pages/Notifications.tsx:446
|
||||
#: src/pages/UserDumps.tsx:54
|
||||
#: src/pages/UserPlaylists.tsx:345
|
||||
#: src/pages/UserPublicProfile.tsx:1064
|
||||
#: src/pages/UserPublicProfile.tsx:1106
|
||||
#: src/pages/UserPublicProfile.tsx:1151
|
||||
#: src/pages/UserPublicProfile.tsx:1067
|
||||
#: src/pages/UserPublicProfile.tsx:1109
|
||||
#: src/pages/UserPublicProfile.tsx:1154
|
||||
#: src/pages/UserUpvoted.tsx:125
|
||||
msgid "Loading…"
|
||||
msgstr "Loading…"
|
||||
@@ -794,8 +794,8 @@ msgstr "Log in to like"
|
||||
msgid "Log in to vote"
|
||||
msgstr "Log in to vote"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:802
|
||||
#: src/pages/UserPublicProfile.tsx:898
|
||||
#: src/pages/UserPublicProfile.tsx:805
|
||||
#: src/pages/UserPublicProfile.tsx:901
|
||||
msgid "Log out"
|
||||
msgstr "Log out"
|
||||
|
||||
@@ -807,7 +807,7 @@ msgstr "Logging in…"
|
||||
msgid "Login failed"
|
||||
msgstr "Login failed"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:956
|
||||
#: src/pages/UserPublicProfile.tsx:959
|
||||
msgid "Manage"
|
||||
msgstr "Manage"
|
||||
|
||||
@@ -815,7 +815,7 @@ msgstr "Manage"
|
||||
msgid "Max 50 MB"
|
||||
msgstr "Max 50 MB"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1603
|
||||
#: src/pages/UserPublicProfile.tsx:1645
|
||||
msgid "Moderator"
|
||||
msgstr "Moderator"
|
||||
|
||||
@@ -829,7 +829,7 @@ msgid "new"
|
||||
msgstr "new"
|
||||
|
||||
#: src/components/FeedTabBar.tsx:31
|
||||
#: src/pages/UserPublicProfile.tsx:1295
|
||||
#: src/pages/UserPublicProfile.tsx:1305
|
||||
msgid "New"
|
||||
msgstr "New"
|
||||
|
||||
@@ -837,7 +837,7 @@ msgstr "New"
|
||||
#: src/components/DumpFab.tsx:65
|
||||
#: src/components/DumpFab.tsx:66
|
||||
#: src/pages/UserDumps.tsx:88
|
||||
#: src/pages/UserPublicProfile.tsx:1382
|
||||
#: src/pages/UserPublicProfile.tsx:1424
|
||||
msgid "New dump"
|
||||
msgstr "New dump"
|
||||
|
||||
@@ -878,15 +878,15 @@ msgid "No emoji found."
|
||||
msgstr "No emoji found."
|
||||
|
||||
#: src/pages/UserPlaylists.tsx:442
|
||||
#: src/pages/UserPublicProfile.tsx:1119
|
||||
#: src/pages/UserPublicProfile.tsx:1122
|
||||
msgid "No followed playlists yet."
|
||||
msgstr "No followed playlists yet."
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1164
|
||||
#: src/pages/UserPublicProfile.tsx:1167
|
||||
msgid "No invitees yet."
|
||||
msgstr "No invitees yet."
|
||||
|
||||
#: src/components/ChatModal.tsx:558
|
||||
#: src/components/ChatModal.tsx:592
|
||||
msgid "No messages yet. Say hello!"
|
||||
msgstr "No messages yet. Say hello!"
|
||||
|
||||
@@ -900,7 +900,7 @@ msgstr "No playlists match \"{q}\"."
|
||||
|
||||
#: src/components/PlaylistMembershipPanel.tsx:34
|
||||
#: src/pages/UserPlaylists.tsx:400
|
||||
#: src/pages/UserPublicProfile.tsx:1030
|
||||
#: src/pages/UserPublicProfile.tsx:1033
|
||||
msgid "No playlists yet."
|
||||
msgstr "No playlists yet."
|
||||
|
||||
@@ -908,14 +908,14 @@ msgstr "No playlists yet."
|
||||
msgid "No users match \"{q}\"."
|
||||
msgstr "No users match \"{q}\"."
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1077
|
||||
#: src/pages/UserPublicProfile.tsx:1080
|
||||
msgid "Not following anyone yet."
|
||||
msgstr "Not following anyone yet."
|
||||
|
||||
#: src/pages/Notifications.tsx:381
|
||||
#: src/pages/UserDumps.tsx:99
|
||||
#: src/pages/UserPublicProfile.tsx:1393
|
||||
#: src/pages/UserPublicProfile.tsx:1515
|
||||
#: src/pages/UserPublicProfile.tsx:1435
|
||||
#: src/pages/UserPublicProfile.tsx:1557
|
||||
#: src/pages/UserUpvoted.tsx:159
|
||||
msgid "Nothing here yet."
|
||||
msgstr "Nothing here yet."
|
||||
@@ -923,6 +923,7 @@ msgstr "Nothing here yet."
|
||||
#: src/components/NotificationBell.tsx:42
|
||||
#: src/pages/Notifications.tsx:265
|
||||
#: src/pages/Notifications.tsx:359
|
||||
#: src/pages/UserPublicProfile.tsx:1327
|
||||
msgid "Notifications"
|
||||
msgstr "Notifications"
|
||||
|
||||
@@ -930,6 +931,14 @@ msgstr "Notifications"
|
||||
msgid "Notifications ({unreadNotificationCount} unread)"
|
||||
msgstr "Notifications ({unreadNotificationCount} unread)"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1351
|
||||
msgid "Off"
|
||||
msgstr "Off"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1344
|
||||
msgid "On"
|
||||
msgstr "On"
|
||||
|
||||
#: src/components/SearchBar.tsx:96
|
||||
msgid "Open search"
|
||||
msgstr "Open search"
|
||||
@@ -943,7 +952,7 @@ msgid "Page not found"
|
||||
msgstr "Page not found"
|
||||
|
||||
#: src/pages/UserLogin.tsx:76
|
||||
#: src/pages/UserPublicProfile.tsx:1184
|
||||
#: src/pages/UserPublicProfile.tsx:1187
|
||||
msgid "Password"
|
||||
msgstr "Password"
|
||||
|
||||
@@ -982,13 +991,13 @@ msgstr "Playlist title"
|
||||
#: src/components/UserMenu.tsx:62
|
||||
#: src/pages/Search.tsx:177
|
||||
#: src/pages/UserPlaylists.tsx:371
|
||||
#: src/pages/UserPublicProfile.tsx:949
|
||||
#: src/pages/UserPublicProfile.tsx:952
|
||||
msgid "Playlists"
|
||||
msgstr "Playlists"
|
||||
|
||||
#. placeholder {0}: playlists.items.length
|
||||
#. placeholder {1}: playlists.hasMore ? "+" : ""
|
||||
#: src/pages/UserPublicProfile.tsx:999
|
||||
#: src/pages/UserPublicProfile.tsx:1002
|
||||
msgid "Playlists ({0}{1})"
|
||||
msgstr "Playlists ({0}{1})"
|
||||
|
||||
@@ -1090,7 +1099,7 @@ msgstr "Replace file"
|
||||
msgid "Reply"
|
||||
msgstr "Reply"
|
||||
|
||||
#: src/components/ChatModal.tsx:614
|
||||
#: src/components/ChatModal.tsx:648
|
||||
msgid "Replying to"
|
||||
msgstr "Replying to"
|
||||
|
||||
@@ -1121,7 +1130,7 @@ msgstr "Reset to the suggested title"
|
||||
msgid "Retry"
|
||||
msgstr "Retry"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1658
|
||||
#: src/pages/UserPublicProfile.tsx:1700
|
||||
msgid "Role"
|
||||
msgstr "Role"
|
||||
|
||||
@@ -1130,8 +1139,8 @@ msgstr "Role"
|
||||
#: src/pages/Dump.tsx:398
|
||||
#: src/pages/DumpEdit.tsx:487
|
||||
#: src/pages/PlaylistDetail.tsx:930
|
||||
#: src/pages/UserPublicProfile.tsx:1725
|
||||
#: src/pages/UserPublicProfile.tsx:1798
|
||||
#: src/pages/UserPublicProfile.tsx:1767
|
||||
#: src/pages/UserPublicProfile.tsx:1840
|
||||
msgid "Save"
|
||||
msgstr "Save"
|
||||
|
||||
@@ -1140,8 +1149,8 @@ msgstr "Save"
|
||||
#: src/pages/Dump.tsx:397
|
||||
#: src/pages/PlaylistDetail.tsx:926
|
||||
#: src/pages/ResetPassword.tsx:126
|
||||
#: src/pages/UserPublicProfile.tsx:1722
|
||||
#: src/pages/UserPublicProfile.tsx:1795
|
||||
#: src/pages/UserPublicProfile.tsx:1764
|
||||
#: src/pages/UserPublicProfile.tsx:1837
|
||||
msgid "Saving…"
|
||||
msgstr "Saving…"
|
||||
|
||||
@@ -1163,7 +1172,7 @@ msgstr "Search failed"
|
||||
msgid "Searching…"
|
||||
msgstr "Searching…"
|
||||
|
||||
#: src/components/ChatModal.tsx:660
|
||||
#: src/components/ChatModal.tsx:694
|
||||
msgid "Send"
|
||||
msgstr "Send"
|
||||
|
||||
@@ -1184,15 +1193,15 @@ msgstr "Server unreachable"
|
||||
msgid "Set new password"
|
||||
msgstr "Set new password"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:953
|
||||
#: src/pages/UserPublicProfile.tsx:956
|
||||
msgid "Settings"
|
||||
msgstr "Settings"
|
||||
|
||||
#: src/components/ChatModal.tsx:535
|
||||
#: src/components/ChatModal.tsx:569
|
||||
msgid "Several people are typing…"
|
||||
msgstr "Several people are typing…"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:162
|
||||
#: src/pages/UserPublicProfile.tsx:164
|
||||
msgid "Single use, valid for 7 days."
|
||||
msgstr "Single use, valid for 7 days."
|
||||
|
||||
@@ -1205,11 +1214,15 @@ msgstr "slug"
|
||||
msgid "Something went wrong"
|
||||
msgstr "Something went wrong"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1332
|
||||
msgid "Sounds"
|
||||
msgstr "Sounds"
|
||||
|
||||
#: src/components/DumpCreateModal.tsx:707
|
||||
msgid "Start fresh"
|
||||
msgstr "Start fresh"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1203
|
||||
#: src/pages/UserPublicProfile.tsx:1206
|
||||
msgid "Style"
|
||||
msgstr "Style"
|
||||
|
||||
@@ -1261,7 +1274,7 @@ msgstr "Today"
|
||||
msgid "Track {0}"
|
||||
msgstr "Track {0}"
|
||||
|
||||
#: src/components/ChatModal.tsx:637
|
||||
#: src/components/ChatModal.tsx:671
|
||||
msgid "Type a message…"
|
||||
msgstr "Type a message…"
|
||||
|
||||
@@ -1277,7 +1290,7 @@ msgstr "Unfollow {targetUsername}"
|
||||
msgid "Unfollow playlist"
|
||||
msgstr "Unfollow playlist"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:748
|
||||
#: src/pages/UserPublicProfile.tsx:751
|
||||
msgid "Upload failed"
|
||||
msgstr "Upload failed"
|
||||
|
||||
@@ -1300,7 +1313,7 @@ msgstr "Upvoted"
|
||||
|
||||
#. placeholder {0}: votes.items.length
|
||||
#. placeholder {1}: votes.hasMore ? "+" : ""
|
||||
#: src/pages/UserPublicProfile.tsx:979
|
||||
#: src/pages/UserPublicProfile.tsx:982
|
||||
msgid "Upvoted ({0}{1})"
|
||||
msgstr "Upvoted ({0}{1})"
|
||||
|
||||
@@ -1316,11 +1329,11 @@ msgstr "URL is required."
|
||||
msgid "URL slug"
|
||||
msgstr "URL slug"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1602
|
||||
#: src/pages/UserPublicProfile.tsx:1644
|
||||
msgid "User"
|
||||
msgstr "User"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1321
|
||||
#: src/pages/UserPublicProfile.tsx:1363
|
||||
msgid "User management"
|
||||
msgstr "User management"
|
||||
|
||||
@@ -1337,11 +1350,11 @@ msgstr "Username"
|
||||
msgid "Users"
|
||||
msgstr "Users"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1049
|
||||
#: src/pages/UserPublicProfile.tsx:1092
|
||||
#: src/pages/UserPublicProfile.tsx:1134
|
||||
#: src/pages/UserPublicProfile.tsx:1414
|
||||
#: src/pages/UserPublicProfile.tsx:1545
|
||||
#: src/pages/UserPublicProfile.tsx:1052
|
||||
#: src/pages/UserPublicProfile.tsx:1095
|
||||
#: src/pages/UserPublicProfile.tsx:1137
|
||||
#: src/pages/UserPublicProfile.tsx:1456
|
||||
#: src/pages/UserPublicProfile.tsx:1587
|
||||
msgid "View all →"
|
||||
msgstr "View all →"
|
||||
|
||||
@@ -1354,8 +1367,8 @@ msgstr "View dump →"
|
||||
msgid "What makes it worth it?"
|
||||
msgstr "What makes it worth it?"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:933
|
||||
#: src/pages/UserPublicProfile.tsx:1787
|
||||
#: src/pages/UserPublicProfile.tsx:936
|
||||
#: src/pages/UserPublicProfile.tsx:1829
|
||||
msgid "Who am I?"
|
||||
msgstr "Who am I?"
|
||||
|
||||
@@ -1391,7 +1404,7 @@ msgstr "You'll be notified when someone follows your playlists, upvotes your dum
|
||||
|
||||
#: src/pages/index/FollowedFeed.tsx:116
|
||||
#: src/pages/index/HotFeed.tsx:69
|
||||
#: src/pages/index/JournalFeed.tsx:67
|
||||
#: src/pages/index/JournalFeed.tsx:78
|
||||
#: src/pages/index/NewFeed.tsx:69
|
||||
#: src/pages/Search.tsx:249
|
||||
#: src/pages/UserDumps.tsx:126
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -35,12 +35,12 @@ msgstr "{0, plural, one {et # reco plus ancienne} other {et # recos plus ancienn
|
||||
|
||||
#. placeholder {0}: typingNames[0]
|
||||
#. placeholder {1}: typingNames[1]
|
||||
#: src/components/ChatModal.tsx:534
|
||||
#: src/components/ChatModal.tsx:568
|
||||
msgid "{0} and {1} are typing…"
|
||||
msgstr "{0} et {1} sont en train d'écrire…"
|
||||
|
||||
#. placeholder {0}: typingNames[0]
|
||||
#: src/components/ChatModal.tsx:532
|
||||
#: src/components/ChatModal.tsx:566
|
||||
msgid "{0} is typing…"
|
||||
msgstr "{0} est en train d'écrire…"
|
||||
|
||||
@@ -60,7 +60,7 @@ msgstr "{visibleCount, plural, one {# commentaire} other {# commentaires}}"
|
||||
|
||||
#: src/components/DumpCreateModal.tsx:936
|
||||
#: src/pages/PlaylistDetail.tsx:572
|
||||
#: src/pages/UserPublicProfile.tsx:798
|
||||
#: src/pages/UserPublicProfile.tsx:801
|
||||
msgid "← Back"
|
||||
msgstr "← Retour"
|
||||
|
||||
@@ -76,7 +76,7 @@ msgstr "← Retour à toutes les recos"
|
||||
msgid "← Back to profile"
|
||||
msgstr "← Retour au profil"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:134
|
||||
#: src/pages/UserPublicProfile.tsx:136
|
||||
msgid "+ Invite someone"
|
||||
msgstr "+ Inviter quelqu'un"
|
||||
|
||||
@@ -142,7 +142,7 @@ msgstr "un commentaire"
|
||||
msgid "a post"
|
||||
msgstr "une publication"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1179
|
||||
#: src/pages/UserPublicProfile.tsx:1182
|
||||
msgid "Account"
|
||||
msgstr "Compte"
|
||||
|
||||
@@ -155,7 +155,7 @@ msgstr "Ajouter un commentaire…"
|
||||
msgid "Add category"
|
||||
msgstr "Ajouter une catégorie"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:876
|
||||
#: src/pages/UserPublicProfile.tsx:879
|
||||
msgid "Add email…"
|
||||
msgstr "Ajouter un e-mail…"
|
||||
|
||||
@@ -164,7 +164,7 @@ msgstr "Ajouter un e-mail…"
|
||||
msgid "Add to playlist"
|
||||
msgstr "Ajouter à la collection"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1604
|
||||
#: src/pages/UserPublicProfile.tsx:1646
|
||||
msgid "Admin"
|
||||
msgstr "Administrateur"
|
||||
|
||||
@@ -183,7 +183,7 @@ msgstr "Déjà recommandé par {0} {1}"
|
||||
msgid "Already have an account? <0>Log in</0>"
|
||||
msgstr "Vous avez déjà un compte ? <0>Se connecter</0>"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1198
|
||||
#: src/pages/UserPublicProfile.tsx:1201
|
||||
msgid "Appearance"
|
||||
msgstr "Apparence"
|
||||
|
||||
@@ -194,7 +194,7 @@ msgstr "Apparence"
|
||||
msgid "At least {0} characters"
|
||||
msgstr "Au moins {0} caractères"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1253
|
||||
#: src/pages/UserPublicProfile.tsx:1263
|
||||
msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
@@ -203,8 +203,8 @@ msgstr "Auto"
|
||||
msgid "Back to login"
|
||||
msgstr "Retour à la connexion"
|
||||
|
||||
#: src/contexts/WSProvider.tsx:270
|
||||
#: src/contexts/WSProvider.tsx:576
|
||||
#: src/contexts/WSProvider.tsx:292
|
||||
#: src/contexts/WSProvider.tsx:608
|
||||
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."
|
||||
|
||||
@@ -218,8 +218,8 @@ msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les vo
|
||||
#: src/pages/Dump.tsx:406
|
||||
#: src/pages/DumpEdit.tsx:484
|
||||
#: src/pages/PlaylistDetail.tsx:923
|
||||
#: src/pages/UserPublicProfile.tsx:1733
|
||||
#: src/pages/UserPublicProfile.tsx:1806
|
||||
#: src/pages/UserPublicProfile.tsx:1775
|
||||
#: src/pages/UserPublicProfile.tsx:1848
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
@@ -227,8 +227,8 @@ msgstr "Annuler"
|
||||
msgid "Cancel removal"
|
||||
msgstr "Annuler la suppression"
|
||||
|
||||
#: src/components/ChatModal.tsx:624
|
||||
#: src/components/ChatModal.tsx:625
|
||||
#: src/components/ChatModal.tsx:658
|
||||
#: src/components/ChatModal.tsx:659
|
||||
msgid "Cancel reply"
|
||||
msgstr "Annuler la réponse"
|
||||
|
||||
@@ -237,7 +237,7 @@ msgid "Cancel search"
|
||||
msgstr "Annuler la recherche"
|
||||
|
||||
#: src/components/CategorySelect.tsx:22
|
||||
#: src/pages/UserPublicProfile.tsx:1336
|
||||
#: src/pages/UserPublicProfile.tsx:1378
|
||||
msgid "Categories"
|
||||
msgstr "Catégories"
|
||||
|
||||
@@ -245,7 +245,7 @@ msgstr "Catégories"
|
||||
msgid "Category"
|
||||
msgstr "Catégorie"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:825
|
||||
#: src/pages/UserPublicProfile.tsx:828
|
||||
msgid "Change avatar"
|
||||
msgstr "Changer l'avatar"
|
||||
|
||||
@@ -254,14 +254,14 @@ msgstr "Changer l'avatar"
|
||||
msgid "Change password"
|
||||
msgstr "Changer le mot de passe"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1191
|
||||
#: src/pages/UserPublicProfile.tsx:1194
|
||||
msgid "Change password…"
|
||||
msgstr "Changer le mot de passe…"
|
||||
|
||||
#: src/components/ChatButton.tsx:17
|
||||
#: src/components/ChatFab.tsx:49
|
||||
#: src/components/ChatFab.tsx:50
|
||||
#: src/components/ChatModal.tsx:538
|
||||
#: src/components/ChatModal.tsx:572
|
||||
msgid "Chat"
|
||||
msgstr "Tribune"
|
||||
|
||||
@@ -295,7 +295,7 @@ msgstr "Fermer sans publier ? Le fichier joint sera perdu — ce que vous avez
|
||||
msgid "Collapse player"
|
||||
msgstr "Réduire le lecteur"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1245
|
||||
#: src/pages/UserPublicProfile.tsx:1255
|
||||
msgid "Color scheme"
|
||||
msgstr "Thème de couleur"
|
||||
|
||||
@@ -304,11 +304,11 @@ msgstr "Thème de couleur"
|
||||
msgid "Confirm new password"
|
||||
msgstr "Confirmer le nouveau mot de passe"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:159
|
||||
#: src/pages/UserPublicProfile.tsx:161
|
||||
msgid "Copied!"
|
||||
msgstr "Copié !"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:159
|
||||
#: src/pages/UserPublicProfile.tsx:161
|
||||
msgid "Copy"
|
||||
msgstr "Copier"
|
||||
|
||||
@@ -350,11 +350,11 @@ msgstr "Création…"
|
||||
msgid "Current password"
|
||||
msgstr "Mot de passe actuel"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1267
|
||||
#: src/pages/UserPublicProfile.tsx:1277
|
||||
msgid "Dark"
|
||||
msgstr "Sombre"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1280
|
||||
#: src/pages/UserPublicProfile.tsx:1290
|
||||
msgid "Default tab"
|
||||
msgstr "Onglet par défaut"
|
||||
|
||||
@@ -446,13 +446,13 @@ msgstr "Recommandé !"
|
||||
|
||||
#: src/pages/Search.tsx:174
|
||||
#: src/pages/UserDumps.tsx:80
|
||||
#: src/pages/UserPublicProfile.tsx:948
|
||||
#: src/pages/UserPublicProfile.tsx:951
|
||||
msgid "Dumps"
|
||||
msgstr "Recos"
|
||||
|
||||
#. placeholder {0}: dumps.items.length
|
||||
#. placeholder {1}: dumps.hasMore ? "+" : ""
|
||||
#: src/pages/UserPublicProfile.tsx:968
|
||||
#: src/pages/UserPublicProfile.tsx:971
|
||||
msgid "Dumps ({0}{1})"
|
||||
msgstr "Recos ({0}{1})"
|
||||
|
||||
@@ -530,9 +530,9 @@ msgstr "Impossible de créer la collection"
|
||||
msgid "Failed to delete"
|
||||
msgstr "Suppression échouée"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:104
|
||||
#: src/pages/UserPublicProfile.tsx:107
|
||||
#: src/pages/UserPublicProfile.tsx:168
|
||||
#: src/pages/UserPublicProfile.tsx:106
|
||||
#: src/pages/UserPublicProfile.tsx:109
|
||||
#: src/pages/UserPublicProfile.tsx:170
|
||||
msgid "Failed to generate invite"
|
||||
msgstr "Impossible de générer une invitation"
|
||||
|
||||
@@ -541,9 +541,9 @@ msgstr "Impossible de générer une invitation"
|
||||
#: src/pages/index/JournalFeed.tsx:33
|
||||
#: src/pages/index/NewFeed.tsx:36
|
||||
#: src/pages/Notifications.tsx:374
|
||||
#: src/pages/UserPublicProfile.tsx:1070
|
||||
#: src/pages/UserPublicProfile.tsx:1112
|
||||
#: src/pages/UserPublicProfile.tsx:1157
|
||||
#: src/pages/UserPublicProfile.tsx:1073
|
||||
#: src/pages/UserPublicProfile.tsx:1115
|
||||
#: src/pages/UserPublicProfile.tsx:1160
|
||||
msgid "Failed to load"
|
||||
msgstr "Chargement échoué"
|
||||
|
||||
@@ -560,8 +560,8 @@ msgid "Failed to post reply"
|
||||
msgstr "Impossible de publier la réponse"
|
||||
|
||||
#: src/pages/PlaylistDetail.tsx:911
|
||||
#: src/pages/UserPublicProfile.tsx:1736
|
||||
#: src/pages/UserPublicProfile.tsx:1808
|
||||
#: src/pages/UserPublicProfile.tsx:1778
|
||||
#: src/pages/UserPublicProfile.tsx:1850
|
||||
msgid "Failed to save"
|
||||
msgstr "Enregistrement échoué"
|
||||
|
||||
@@ -573,15 +573,15 @@ msgstr "Échec de l’enregistrement de la catégorie"
|
||||
msgid "Failed to save edit"
|
||||
msgstr "Impossible d'enregistrer la modification"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:885
|
||||
#: src/pages/UserPublicProfile.tsx:888
|
||||
msgid "Failed to update avatar"
|
||||
msgstr "Impossible de mettre à jour l'avatar"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1674
|
||||
#: src/pages/UserPublicProfile.tsx:1716
|
||||
msgid "Failed to update role"
|
||||
msgstr "Erreur lors de la mise à jour du rôle"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1275
|
||||
#: src/pages/UserPublicProfile.tsx:1285
|
||||
msgid "Feeds"
|
||||
msgstr "Flux"
|
||||
|
||||
@@ -615,8 +615,8 @@ msgid "Follow some users to see their dumps here."
|
||||
msgstr "Suivez des utilisateurs pour voir leurs recos ici."
|
||||
|
||||
#: src/components/FeedTabBar.tsx:34
|
||||
#: src/pages/UserPublicProfile.tsx:950
|
||||
#: src/pages/UserPublicProfile.tsx:1309
|
||||
#: src/pages/UserPublicProfile.tsx:953
|
||||
#: src/pages/UserPublicProfile.tsx:1319
|
||||
msgid "Followed"
|
||||
msgstr "Suivi"
|
||||
|
||||
@@ -626,13 +626,13 @@ msgstr "Suivi"
|
||||
msgid "Followed ({0}{1})"
|
||||
msgstr "Suivies ({0}{1})"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1101
|
||||
#: src/pages/UserPublicProfile.tsx:1104
|
||||
msgid "Followed playlists"
|
||||
msgstr "Collections suivies"
|
||||
|
||||
#: src/components/FollowButton.tsx:37
|
||||
#: src/components/FollowButton.tsx:64
|
||||
#: src/pages/UserPublicProfile.tsx:1059
|
||||
#: src/pages/UserPublicProfile.tsx:1062
|
||||
msgid "Following"
|
||||
msgstr "Abonné"
|
||||
|
||||
@@ -657,7 +657,7 @@ msgid "Go to login"
|
||||
msgstr "Aller à la connexion"
|
||||
|
||||
#: src/components/FeedTabBar.tsx:30
|
||||
#: src/pages/UserPublicProfile.tsx:1288
|
||||
#: src/pages/UserPublicProfile.tsx:1298
|
||||
msgid "Hot"
|
||||
msgstr "Tendances"
|
||||
|
||||
@@ -677,22 +677,22 @@ msgstr "Invitation invalide"
|
||||
msgid "Invalid link"
|
||||
msgstr "Lien invalide"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:142
|
||||
#: src/pages/UserPublicProfile.tsx:149
|
||||
#: src/pages/UserPublicProfile.tsx:144
|
||||
#: src/pages/UserPublicProfile.tsx:151
|
||||
msgid "Invite link"
|
||||
msgstr "Lien d'invitation"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:847
|
||||
#: src/pages/UserPublicProfile.tsx:850
|
||||
msgid "invited by"
|
||||
msgstr "invité par"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:951
|
||||
#: src/pages/UserPublicProfile.tsx:1146
|
||||
#: src/pages/UserPublicProfile.tsx:954
|
||||
#: src/pages/UserPublicProfile.tsx:1149
|
||||
msgid "Invitees"
|
||||
msgstr "Invités"
|
||||
|
||||
#: src/components/FeedTabBar.tsx:32
|
||||
#: src/pages/UserPublicProfile.tsx:1302
|
||||
#: src/pages/UserPublicProfile.tsx:1312
|
||||
msgid "Journal"
|
||||
msgstr "Journal"
|
||||
|
||||
@@ -700,7 +700,7 @@ msgstr "Journal"
|
||||
msgid "Keep editing"
|
||||
msgstr "Continuer l'édition"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1260
|
||||
#: src/pages/UserPublicProfile.tsx:1270
|
||||
msgid "Light"
|
||||
msgstr "Clair"
|
||||
|
||||
@@ -716,7 +716,7 @@ msgstr "Lien"
|
||||
msgid "Link or file"
|
||||
msgstr "Lien ou fichier"
|
||||
|
||||
#: src/contexts/WSProvider.tsx:575
|
||||
#: src/contexts/WSProvider.tsx:607
|
||||
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||
msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"
|
||||
|
||||
@@ -729,7 +729,7 @@ msgstr "Mises à jour en direct indisponibles."
|
||||
msgid "Load more"
|
||||
msgstr "Charger plus"
|
||||
|
||||
#: src/components/ChatModal.tsx:551
|
||||
#: src/components/ChatModal.tsx:585
|
||||
msgid "Load older messages"
|
||||
msgstr "Charger les messages plus anciens"
|
||||
|
||||
@@ -740,7 +740,7 @@ msgstr "Chargement de la reco…"
|
||||
|
||||
#: src/pages/index/FollowedFeed.tsx:111
|
||||
#: src/pages/index/HotFeed.tsx:64
|
||||
#: src/pages/index/JournalFeed.tsx:62
|
||||
#: src/pages/index/JournalFeed.tsx:73
|
||||
#: src/pages/index/NewFeed.tsx:64
|
||||
#: src/pages/Search.tsx:244
|
||||
#: src/pages/UserDumps.tsx:121
|
||||
@@ -754,12 +754,12 @@ msgstr "Chargement…"
|
||||
msgid "Loading playlist…"
|
||||
msgstr "Chargement de la collection…"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:781
|
||||
#: src/pages/UserPublicProfile.tsx:784
|
||||
msgid "Loading profile…"
|
||||
msgstr "Chargement du profil…"
|
||||
|
||||
#: src/components/CategoryManager.tsx:52
|
||||
#: src/components/ChatModal.tsx:550
|
||||
#: src/components/ChatModal.tsx:584
|
||||
#: src/components/PlaylistMembershipPanel.tsx:28
|
||||
#: src/components/TextEditor.tsx:311
|
||||
#: src/components/UserListPopover.tsx:192
|
||||
@@ -772,9 +772,9 @@ msgstr "Chargement du profil…"
|
||||
#: src/pages/Notifications.tsx:446
|
||||
#: src/pages/UserDumps.tsx:54
|
||||
#: src/pages/UserPlaylists.tsx:345
|
||||
#: src/pages/UserPublicProfile.tsx:1064
|
||||
#: src/pages/UserPublicProfile.tsx:1106
|
||||
#: src/pages/UserPublicProfile.tsx:1151
|
||||
#: src/pages/UserPublicProfile.tsx:1067
|
||||
#: src/pages/UserPublicProfile.tsx:1109
|
||||
#: src/pages/UserPublicProfile.tsx:1154
|
||||
#: src/pages/UserUpvoted.tsx:125
|
||||
msgid "Loading…"
|
||||
msgstr "Chargement…"
|
||||
@@ -794,8 +794,8 @@ msgstr "Se connecter pour aimer"
|
||||
msgid "Log in to vote"
|
||||
msgstr "Se connecter pour voter"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:802
|
||||
#: src/pages/UserPublicProfile.tsx:898
|
||||
#: src/pages/UserPublicProfile.tsx:805
|
||||
#: src/pages/UserPublicProfile.tsx:901
|
||||
msgid "Log out"
|
||||
msgstr "Se déconnecter"
|
||||
|
||||
@@ -807,7 +807,7 @@ msgstr "Connexion…"
|
||||
msgid "Login failed"
|
||||
msgstr "Connexion échouée"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:956
|
||||
#: src/pages/UserPublicProfile.tsx:959
|
||||
msgid "Manage"
|
||||
msgstr "Administration"
|
||||
|
||||
@@ -815,7 +815,7 @@ msgstr "Administration"
|
||||
msgid "Max 50 MB"
|
||||
msgstr "Max 50 Mo"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1603
|
||||
#: src/pages/UserPublicProfile.tsx:1645
|
||||
msgid "Moderator"
|
||||
msgstr "Modérateur"
|
||||
|
||||
@@ -829,7 +829,7 @@ msgid "new"
|
||||
msgstr "nouveau"
|
||||
|
||||
#: src/components/FeedTabBar.tsx:31
|
||||
#: src/pages/UserPublicProfile.tsx:1295
|
||||
#: src/pages/UserPublicProfile.tsx:1305
|
||||
msgid "New"
|
||||
msgstr "Nouveau"
|
||||
|
||||
@@ -837,7 +837,7 @@ msgstr "Nouveau"
|
||||
#: src/components/DumpFab.tsx:65
|
||||
#: src/components/DumpFab.tsx:66
|
||||
#: src/pages/UserDumps.tsx:88
|
||||
#: src/pages/UserPublicProfile.tsx:1382
|
||||
#: src/pages/UserPublicProfile.tsx:1424
|
||||
msgid "New dump"
|
||||
msgstr "Nouvelle reco"
|
||||
|
||||
@@ -878,15 +878,15 @@ msgid "No emoji found."
|
||||
msgstr "Aucun emoji trouvé."
|
||||
|
||||
#: src/pages/UserPlaylists.tsx:442
|
||||
#: src/pages/UserPublicProfile.tsx:1119
|
||||
#: src/pages/UserPublicProfile.tsx:1122
|
||||
msgid "No followed playlists yet."
|
||||
msgstr "Pas encore de collections suivies."
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1164
|
||||
#: src/pages/UserPublicProfile.tsx:1167
|
||||
msgid "No invitees yet."
|
||||
msgstr "Aucun invité pour le moment."
|
||||
|
||||
#: src/components/ChatModal.tsx:558
|
||||
#: src/components/ChatModal.tsx:592
|
||||
msgid "No messages yet. Say hello!"
|
||||
msgstr "Aucun message pour l'instant. Dites bonjour !"
|
||||
|
||||
@@ -900,7 +900,7 @@ msgstr "Aucune collection ne correspond à « {q} »."
|
||||
|
||||
#: src/components/PlaylistMembershipPanel.tsx:34
|
||||
#: src/pages/UserPlaylists.tsx:400
|
||||
#: src/pages/UserPublicProfile.tsx:1030
|
||||
#: src/pages/UserPublicProfile.tsx:1033
|
||||
msgid "No playlists yet."
|
||||
msgstr "Pas encore de collections."
|
||||
|
||||
@@ -908,14 +908,14 @@ msgstr "Pas encore de collections."
|
||||
msgid "No users match \"{q}\"."
|
||||
msgstr "Aucun utilisateur ne correspond à « {q} »."
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1077
|
||||
#: src/pages/UserPublicProfile.tsx:1080
|
||||
msgid "Not following anyone yet."
|
||||
msgstr "Aucun abonnement pour le moment."
|
||||
|
||||
#: src/pages/Notifications.tsx:381
|
||||
#: src/pages/UserDumps.tsx:99
|
||||
#: src/pages/UserPublicProfile.tsx:1393
|
||||
#: src/pages/UserPublicProfile.tsx:1515
|
||||
#: src/pages/UserPublicProfile.tsx:1435
|
||||
#: src/pages/UserPublicProfile.tsx:1557
|
||||
#: src/pages/UserUpvoted.tsx:159
|
||||
msgid "Nothing here yet."
|
||||
msgstr "Rien ici pour l'instant."
|
||||
@@ -923,6 +923,7 @@ msgstr "Rien ici pour l'instant."
|
||||
#: src/components/NotificationBell.tsx:42
|
||||
#: src/pages/Notifications.tsx:265
|
||||
#: src/pages/Notifications.tsx:359
|
||||
#: src/pages/UserPublicProfile.tsx:1327
|
||||
msgid "Notifications"
|
||||
msgstr "Notifications"
|
||||
|
||||
@@ -930,6 +931,14 @@ msgstr "Notifications"
|
||||
msgid "Notifications ({unreadNotificationCount} unread)"
|
||||
msgstr "Notifications ({unreadNotificationCount} non lues)"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1351
|
||||
msgid "Off"
|
||||
msgstr "Désactivés"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1344
|
||||
msgid "On"
|
||||
msgstr "Activés"
|
||||
|
||||
#: src/components/SearchBar.tsx:96
|
||||
msgid "Open search"
|
||||
msgstr "Ouvrir la recherche"
|
||||
@@ -943,7 +952,7 @@ msgid "Page not found"
|
||||
msgstr "Page introuvable"
|
||||
|
||||
#: src/pages/UserLogin.tsx:76
|
||||
#: src/pages/UserPublicProfile.tsx:1184
|
||||
#: src/pages/UserPublicProfile.tsx:1187
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
@@ -982,13 +991,13 @@ msgstr "Titre de la collection"
|
||||
#: src/components/UserMenu.tsx:62
|
||||
#: src/pages/Search.tsx:177
|
||||
#: src/pages/UserPlaylists.tsx:371
|
||||
#: src/pages/UserPublicProfile.tsx:949
|
||||
#: src/pages/UserPublicProfile.tsx:952
|
||||
msgid "Playlists"
|
||||
msgstr "Collections"
|
||||
|
||||
#. placeholder {0}: playlists.items.length
|
||||
#. placeholder {1}: playlists.hasMore ? "+" : ""
|
||||
#: src/pages/UserPublicProfile.tsx:999
|
||||
#: src/pages/UserPublicProfile.tsx:1002
|
||||
msgid "Playlists ({0}{1})"
|
||||
msgstr "Collections ({0}{1})"
|
||||
|
||||
@@ -1090,7 +1099,7 @@ msgstr "Remplacer le fichier"
|
||||
msgid "Reply"
|
||||
msgstr "Répondre"
|
||||
|
||||
#: src/components/ChatModal.tsx:614
|
||||
#: src/components/ChatModal.tsx:648
|
||||
msgid "Replying to"
|
||||
msgstr "En réponse à"
|
||||
|
||||
@@ -1121,7 +1130,7 @@ msgstr "Rétablir le titre suggéré"
|
||||
msgid "Retry"
|
||||
msgstr "Réessayer"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1658
|
||||
#: src/pages/UserPublicProfile.tsx:1700
|
||||
msgid "Role"
|
||||
msgstr "Rôle"
|
||||
|
||||
@@ -1130,8 +1139,8 @@ msgstr "Rôle"
|
||||
#: src/pages/Dump.tsx:398
|
||||
#: src/pages/DumpEdit.tsx:487
|
||||
#: src/pages/PlaylistDetail.tsx:930
|
||||
#: src/pages/UserPublicProfile.tsx:1725
|
||||
#: src/pages/UserPublicProfile.tsx:1798
|
||||
#: src/pages/UserPublicProfile.tsx:1767
|
||||
#: src/pages/UserPublicProfile.tsx:1840
|
||||
msgid "Save"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
@@ -1140,8 +1149,8 @@ msgstr "Enregistrer"
|
||||
#: src/pages/Dump.tsx:397
|
||||
#: src/pages/PlaylistDetail.tsx:926
|
||||
#: src/pages/ResetPassword.tsx:126
|
||||
#: src/pages/UserPublicProfile.tsx:1722
|
||||
#: src/pages/UserPublicProfile.tsx:1795
|
||||
#: src/pages/UserPublicProfile.tsx:1764
|
||||
#: src/pages/UserPublicProfile.tsx:1837
|
||||
msgid "Saving…"
|
||||
msgstr "Enregistrement…"
|
||||
|
||||
@@ -1163,7 +1172,7 @@ msgstr "Recherche échouée"
|
||||
msgid "Searching…"
|
||||
msgstr "Recherche…"
|
||||
|
||||
#: src/components/ChatModal.tsx:660
|
||||
#: src/components/ChatModal.tsx:694
|
||||
msgid "Send"
|
||||
msgstr "Envoyer"
|
||||
|
||||
@@ -1184,15 +1193,15 @@ msgstr "Serveur inaccessible"
|
||||
msgid "Set new password"
|
||||
msgstr "Définir un nouveau mot de passe"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:953
|
||||
#: src/pages/UserPublicProfile.tsx:956
|
||||
msgid "Settings"
|
||||
msgstr "Paramètres"
|
||||
|
||||
#: src/components/ChatModal.tsx:535
|
||||
#: src/components/ChatModal.tsx:569
|
||||
msgid "Several people are typing…"
|
||||
msgstr "Plusieurs personnes écrivent…"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:162
|
||||
#: src/pages/UserPublicProfile.tsx:164
|
||||
msgid "Single use, valid for 7 days."
|
||||
msgstr "Usage unique, valable 7 jours."
|
||||
|
||||
@@ -1205,11 +1214,15 @@ msgstr "identifiant"
|
||||
msgid "Something went wrong"
|
||||
msgstr "Une erreur est survenue"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1332
|
||||
msgid "Sounds"
|
||||
msgstr "Sons"
|
||||
|
||||
#: src/components/DumpCreateModal.tsx:707
|
||||
msgid "Start fresh"
|
||||
msgstr "Repartir de zéro"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1203
|
||||
#: src/pages/UserPublicProfile.tsx:1206
|
||||
msgid "Style"
|
||||
msgstr "Style"
|
||||
|
||||
@@ -1261,7 +1274,7 @@ msgstr "Aujourd'hui"
|
||||
msgid "Track {0}"
|
||||
msgstr "Piste {0}"
|
||||
|
||||
#: src/components/ChatModal.tsx:637
|
||||
#: src/components/ChatModal.tsx:671
|
||||
msgid "Type a message…"
|
||||
msgstr "Écrivez un message…"
|
||||
|
||||
@@ -1277,7 +1290,7 @@ msgstr "Ne plus suivre {targetUsername}"
|
||||
msgid "Unfollow playlist"
|
||||
msgstr "Ne plus suivre la collection"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:748
|
||||
#: src/pages/UserPublicProfile.tsx:751
|
||||
msgid "Upload failed"
|
||||
msgstr "Envoi échoué"
|
||||
|
||||
@@ -1300,7 +1313,7 @@ msgstr "Voté"
|
||||
|
||||
#. placeholder {0}: votes.items.length
|
||||
#. placeholder {1}: votes.hasMore ? "+" : ""
|
||||
#: src/pages/UserPublicProfile.tsx:979
|
||||
#: src/pages/UserPublicProfile.tsx:982
|
||||
msgid "Upvoted ({0}{1})"
|
||||
msgstr "Votés ({0}{1})"
|
||||
|
||||
@@ -1316,11 +1329,11 @@ msgstr "L'URL est obligatoire."
|
||||
msgid "URL slug"
|
||||
msgstr "Identifiant d’URL"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1602
|
||||
#: src/pages/UserPublicProfile.tsx:1644
|
||||
msgid "User"
|
||||
msgstr "Utilisateur"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1321
|
||||
#: src/pages/UserPublicProfile.tsx:1363
|
||||
msgid "User management"
|
||||
msgstr "Gestion utilisateur"
|
||||
|
||||
@@ -1337,11 +1350,11 @@ msgstr "Nom d'utilisateur"
|
||||
msgid "Users"
|
||||
msgstr "Utilisateurs"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:1049
|
||||
#: src/pages/UserPublicProfile.tsx:1092
|
||||
#: src/pages/UserPublicProfile.tsx:1134
|
||||
#: src/pages/UserPublicProfile.tsx:1414
|
||||
#: src/pages/UserPublicProfile.tsx:1545
|
||||
#: src/pages/UserPublicProfile.tsx:1052
|
||||
#: src/pages/UserPublicProfile.tsx:1095
|
||||
#: src/pages/UserPublicProfile.tsx:1137
|
||||
#: src/pages/UserPublicProfile.tsx:1456
|
||||
#: src/pages/UserPublicProfile.tsx:1587
|
||||
msgid "View all →"
|
||||
msgstr "Tout voir →"
|
||||
|
||||
@@ -1354,8 +1367,8 @@ msgstr "Voir la reco →"
|
||||
msgid "What makes it worth it?"
|
||||
msgstr "Pourquoi on en voudrait ?"
|
||||
|
||||
#: src/pages/UserPublicProfile.tsx:933
|
||||
#: src/pages/UserPublicProfile.tsx:1787
|
||||
#: src/pages/UserPublicProfile.tsx:936
|
||||
#: src/pages/UserPublicProfile.tsx:1829
|
||||
msgid "Who am I?"
|
||||
msgstr "Qui suis-je ?"
|
||||
|
||||
@@ -1391,7 +1404,7 @@ msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vo
|
||||
|
||||
#: src/pages/index/FollowedFeed.tsx:116
|
||||
#: src/pages/index/HotFeed.tsx:69
|
||||
#: src/pages/index/JournalFeed.tsx:67
|
||||
#: src/pages/index/JournalFeed.tsx:78
|
||||
#: src/pages/index/NewFeed.tsx:69
|
||||
#: src/pages/Search.tsx:249
|
||||
#: src/pages/UserDumps.tsx:126
|
||||
|
||||
@@ -42,6 +42,8 @@ import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
import { useTheme } from "../hooks/useTheme.ts";
|
||||
import { useDefaultFeedTab } from "../hooks/useDefaultFeedTab.ts";
|
||||
import { useSoundsEnabled } from "../hooks/useSoundsEnabled.ts";
|
||||
import { previewChime } from "../utils/sound.ts";
|
||||
import { useWS } from "../hooks/useWS.ts";
|
||||
import { useDumpListSync } from "../hooks/useDumpListSync.ts";
|
||||
import { useFading } from "../hooks/useFading.ts";
|
||||
@@ -235,6 +237,7 @@ export function UserPublicProfile() {
|
||||
const navigate = useNavigate();
|
||||
const { user: me, authFetch, login, logout, token } = useAuth();
|
||||
const { style, colorScheme, setStyle, setColorScheme } = useTheme();
|
||||
const [soundsEnabled, setSoundsEnabled] = useSoundsEnabled();
|
||||
const [defaultFeedTab, setDefaultFeedTab] = useDefaultFeedTab();
|
||||
const {
|
||||
voteCounts,
|
||||
@@ -1319,6 +1322,38 @@ export function UserPublicProfile() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="profile-section">
|
||||
<h2 className="profile-section-title">
|
||||
<Trans>Notifications</Trans>
|
||||
</h2>
|
||||
<div className="profile-appearance-grid">
|
||||
<div className="profile-appearance-row">
|
||||
<span className="profile-appearance-label">
|
||||
<Trans>Sounds</Trans>
|
||||
</span>
|
||||
<div className="visibility-toggle">
|
||||
<button
|
||||
type="button"
|
||||
className={soundsEnabled ? "active" : ""}
|
||||
onClick={() => {
|
||||
setSoundsEnabled(true);
|
||||
// Let the user hear what they just turned on.
|
||||
previewChime("notify");
|
||||
}}
|
||||
>
|
||||
<Trans>On</Trans>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={soundsEnabled ? "" : "active"}
|
||||
onClick={() => setSoundsEnabled(false)}
|
||||
>
|
||||
<Trans>Off</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
123
src/utils/sound.ts
Normal file
123
src/utils/sound.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// Light UI sounds for live notifications and chat, synthesised with Web Audio
|
||||
// so there's no asset to ship or cache. Two voices: a single soft blip for a
|
||||
// chat message, and a two-note rising chime for a notification (a mention, a
|
||||
// follow, an upvote…).
|
||||
|
||||
const STORAGE_KEY = "sounds";
|
||||
// Shared across tabs: the id of the last event that chimed, so a user with the
|
||||
// site open in several tabs hears one chime per event rather than one per tab.
|
||||
const CLAIM_KEY = "sounds-last-claim";
|
||||
// A burst of chat messages shouldn't machine-gun blips; one per window is
|
||||
// enough to say "the chat is moving".
|
||||
const CHAT_THROTTLE = 4_000;
|
||||
|
||||
export type ChimeKind = "chat" | "notify";
|
||||
|
||||
export function readSoundsEnabled(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) !== "off";
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeSoundsEnabled(enabled: boolean): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, enabled ? "on" : "off");
|
||||
} catch {
|
||||
// Private mode / blocked storage: the choice just won't persist.
|
||||
}
|
||||
}
|
||||
|
||||
let ctx: AudioContext | null = null;
|
||||
let lastChatAt = 0;
|
||||
|
||||
function audio(): AudioContext | null {
|
||||
if (ctx) return ctx;
|
||||
try {
|
||||
ctx = new AudioContext();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// Browsers keep an AudioContext suspended until the page sees a user gesture.
|
||||
// Resume it on the first one so a chime arriving later — possibly while the
|
||||
// tab is in the background — can actually be heard.
|
||||
function unlock() {
|
||||
const ac = audio();
|
||||
if (ac?.state === "suspended") void ac.resume();
|
||||
globalThis.removeEventListener("pointerdown", unlock);
|
||||
globalThis.removeEventListener("keydown", unlock);
|
||||
}
|
||||
globalThis.addEventListener("pointerdown", unlock);
|
||||
globalThis.addEventListener("keydown", unlock);
|
||||
|
||||
function claim(eventId: string): boolean {
|
||||
try {
|
||||
if (localStorage.getItem(CLAIM_KEY) === eventId) return false;
|
||||
localStorage.setItem(CLAIM_KEY, eventId);
|
||||
} catch {
|
||||
// No storage: every tab chimes, which is the lesser evil.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function tone(
|
||||
ac: AudioContext,
|
||||
freq: number,
|
||||
start: number,
|
||||
duration: number,
|
||||
peak: number,
|
||||
) {
|
||||
const osc = ac.createOscillator();
|
||||
const gain = ac.createGain();
|
||||
osc.type = "sine";
|
||||
osc.frequency.setValueAtTime(freq, start);
|
||||
// Fast attack, exponential tail: a struck, glassy note rather than a beep.
|
||||
gain.gain.setValueAtTime(0.0001, start);
|
||||
gain.gain.exponentialRampToValueAtTime(peak, start + 0.012);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
|
||||
osc.connect(gain).connect(ac.destination);
|
||||
osc.start(start);
|
||||
osc.stop(start + duration + 0.02);
|
||||
}
|
||||
|
||||
function render(kind: ChimeKind) {
|
||||
const ac = audio();
|
||||
if (!ac || ac.state !== "running") return;
|
||||
const now = ac.currentTime;
|
||||
if (kind === "chat") {
|
||||
tone(ac, 1046.5, now, 0.14, 0.05); // C6
|
||||
} else {
|
||||
tone(ac, 659.25, now, 0.22, 0.07); // E5
|
||||
tone(ac, 987.77, now + 0.09, 0.32, 0.06); // B5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a chime for a live event, unless sounds are off, another tab already
|
||||
* chimed for the same event, or (for chat) one played a moment ago.
|
||||
*/
|
||||
export function playChime(kind: ChimeKind, eventId: string): void {
|
||||
if (!readSoundsEnabled()) return;
|
||||
if (kind === "chat") {
|
||||
const now = Date.now();
|
||||
if (now - lastChatAt < CHAT_THROTTLE) return;
|
||||
lastChatAt = now;
|
||||
}
|
||||
if (!claim(`${kind}:${eventId}`)) return;
|
||||
render(kind);
|
||||
}
|
||||
|
||||
/** Plays a chime unconditionally — used to preview it from the settings. */
|
||||
export function previewChime(kind: ChimeKind): void {
|
||||
const ac = audio();
|
||||
if (!ac) return;
|
||||
if (ac.state === "suspended") {
|
||||
void ac.resume().then(() => render(kind));
|
||||
} else {
|
||||
render(kind);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user