This commit is contained in:
nightcordoff
2026-05-27 19:52:44 +02:00
parent d3a9744824
commit bf2de7f9a4
20 changed files with 1568 additions and 153 deletions
Binary file not shown.
+46
View File
@@ -8874,6 +8874,52 @@
"filePath": "src/nightcordplugins/platformSpoofer",
"dirName": "platformSpoofer"
},
{
"hasPatches": false,
"hasCommands": false,
"enabledByDefault": true,
"required": false,
"isModified": false,
"tags": [
],
"name": "PrevNames",
"description": "Shows the username history of a user. Right-click -> PrevNames.",
"authors": [
{
"name": "Nightcord",
"id": "0"
}
],
"dependencies": [
"ContextMenuAPI"
],
"filePath": "src/nightcordplugins/PrevNames",
"dirName": "PrevNames"
},
{
"hasPatches": false,
"hasCommands": false,
"enabledByDefault": true,
"required": false,
"isModified": false,
"tags": [
],
"name": "PrevNames",
"description": "Shows the username history of a user. Right-click -> PrevNames.",
"authors": [
{
"name": "Nightcord",
"id": "0"
}
],
"dependencies": [
"ContextMenuAPI"
],
"filePath": "src/nightcordplugins/PrevNames",
"dirName": "PrevNames"
},
{
"hasPatches": true,
"hasCommands": false,
+25 -9
View File
@@ -353,7 +353,12 @@ function buildBrowserWindowOptions(): BrowserWindowConstructorOptions {
// disable renderer backgrounding to prevent the app from unloading when in the background
backgroundThrottling: false
},
frame: !noFrame,
// FIX 1: quand customTitleBar ou frameless (noFrame=true), utiliser titleBarStyle:"hidden"
// sur Windows/Linux. frame:false seul supprime toute zone draggable OS -> fenetre immobile.
// titleBarStyle:"hidden" garde la zone de drag native invisible mais fonctionnelle.
...(noFrame && process.platform !== "darwin"
? { frame: false, titleBarStyle: "hidden" }
: { frame: !noFrame }),
autoHideMenuBar: enableMenu,
...getWindowBoundsOptions()
};
@@ -424,11 +429,14 @@ function createMainWindow() {
initWindowBoundsListeners(win);
win.on("enter-html-full-screen", () => {
win.setFullScreen(true);
});
// FIX 2: on ne force plus setFullScreen(true) sur enter-html-full-screen.
// L'ancien code causait un blocage : quand Discord passait une video en plein ecran HTML5,
// Electron forcait le fullscreen natif de l'OS -> les utilisateurs ne pouvaient plus
// sortir du fullscreen ou redimensionner la fenetre normalement.
// On laisse Discord gerer son propre fullscreen HTML sans toucher a la fenetre Electron.
win.on("leave-html-full-screen", () => {
win.setFullScreen(false);
// S'assurer que le fullscreen natif est bien quitte si Discord sort du mode HTML FS
if (win.isFullScreen()) win.setFullScreen(false);
});
if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin")
@@ -499,7 +507,13 @@ export async function createWindows() {
if (!startMinimized) {
if (splash) mainWin?.show();
if (State.store.maximized && !isDeckGameMode) mainWin?.maximize();
// FIX 3: on ne maximize que si maximized=true ET windowBounds absent.
// L'ancien code maximizait systematiquement si maximized=true, meme quand l'utilisateur
// avait redimensionne la fenetre -> toujours bloque en plein ecran au demarrage.
const shouldMaximize = State.store.maximized === true
&& !isDeckGameMode
&& !State.store.windowBounds;
if (shouldMaximize) mainWin?.maximize();
}
if (isDeckGameMode) {
@@ -510,9 +524,11 @@ export async function createWindows() {
}
mainWin.once("show", () => {
if (State.store.maximized && !mainWin?.isMaximized() && !isDeckGameMode) {
mainWin?.maximize();
}
const shouldMaximize = State.store.maximized === true
&& !mainWin?.isMaximized()
&& !isDeckGameMode
&& !State.store.windowBounds;
if (shouldMaximize) mainWin?.maximize();
});
});
@@ -127,10 +127,59 @@ export function makeLinksOpenExternally(win: BrowserWindow) {
// Drop the static temp page Discord web loads for the connections popout
if (frameName === "authorize" && searchParams.get("loading") === "true") return { action: "deny" };
// Allow captcha popups to open inside Electron (hCaptcha / reCaptcha)
// Discord opens them via window.open() — they must stay in-process or the
// captcha iframe can never communicate back to Discord.
if (
hostname.includes("hcaptcha.com") ||
hostname.includes("recaptcha.net") ||
hostname.includes("google.com") && pathname.startsWith("/recaptcha") ||
hostname.includes("discord.com") && pathname.startsWith("/cdn-cgi/") ||
// Discord sometimes opens its own captcha flow on discord.com
(DISCORD_HOSTNAMES.includes(hostname) && (pathname.includes("captcha") || searchParams.has("captcha")))
) {
return {
action: "allow",
overrideBrowserWindowOptions: {
width: 500,
height: 600,
frame: true,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
}
}
};
}
return handleExternalUrl(url, protocol);
});
win.webContents.on("did-create-window", (childWin, { frameName, options, url }: any) => {
// Detect captcha windows and handle them gracefully
let isCaptcha = false;
if (url) {
try {
const { hostname, pathname } = new URL(url);
isCaptcha =
hostname.includes("hcaptcha.com") ||
hostname.includes("recaptcha.net") ||
(hostname.includes("google.com") && pathname.startsWith("/recaptcha")) ||
(hostname.includes("discord.com") && pathname.startsWith("/cdn-cgi/")) ||
(DISCORD_HOSTNAMES.includes(hostname) && (pathname.includes("captcha")));
} catch {}
}
if (isCaptcha) {
childWin.setMenuBarVisibility(false);
// Allow the captcha window to open links externally if needed
childWin.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url));
childWin.once("closed", () => childWin.removeAllListeners());
return;
}
let isPopout = frameName.startsWith("DISCORD_");
if (!isPopout) {
File diff suppressed because it is too large Load Diff
@@ -32,13 +32,40 @@ const settings = definePluginSettings({
},
});
// ─── Tick hook — forces a re-render every second ─────────────────────────────
// ─── Global tick — UN seul setInterval partagé par tous les composants ─────────
// BUGFIX: l'ancienne implémentation créait un setInterval PAR composant timestamp
// rendu (50+ messages = 50+ intervals), forçant 50+ re-renders React par seconde
// → freeze complet de Discord. Un seul interval global notifie tous les abonnés.
const tickListeners = new Set<() => void>();
let globalTickInterval: ReturnType<typeof setInterval> | null = null;
function startGlobalTick() {
if (globalTickInterval !== null) return;
globalTickInterval = setInterval(() => {
for (const fn of tickListeners) {
try { fn(); } catch { }
}
}, 1000);
}
function stopGlobalTick() {
if (tickListeners.size > 0) return; // still has subscribers
if (globalTickInterval !== null) {
clearInterval(globalTickInterval);
globalTickInterval = null;
}
}
function useSecondTick() {
const [, tick] = useReducer((n: number) => n + 1, 0);
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
tickListeners.add(tick);
startGlobalTick();
return () => {
tickListeners.delete(tick);
stopGlobalTick();
};
}, []);
}
@@ -79,6 +106,15 @@ export default definePlugin({
renderTimestamp,
stop() {
// Cleanup global tick on plugin disable
tickListeners.clear();
if (globalTickInterval !== null) {
clearInterval(globalTickInterval);
globalTickInterval = null;
}
},
patches: [
// ─── Main Timestamp component (cozy + compact messages + hover tooltip) ─
{
+27 -26
View File
@@ -80,7 +80,6 @@ async function transcribe(blob: Blob): Promise<string> {
form.append("file", blob, "audio.webm");
form.append("model", "whisper-large-v3-turbo");
form.append("response_format", "text");
// Prompt pour orienter Whisper vers du français et éviter les hallucinations "Thank you"
form.append("prompt", "Ceci est une dictée vocale en français. Ne pas traduire en anglais. Ne pas générer de texte si il n'y a que du silence.");
if (language) form.append("language", language);
@@ -124,16 +123,30 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
setErrorMsg(null);
activeRef.current = true;
// Helper to map Discord input device to real WebAudio device
async function getRealInputDeviceId(discordId: string): Promise<string> {
if (!discordId || discordId === "default") return "default";
try {
const devs = MediaEngineStore.getInputDevices();
<<<<<<< HEAD
const selected = devs[discordId];
if (!selected || !selected.name) return "default";
=======
let targetName = "";
if (devs && typeof devs === "object") {
if (Array.isArray(devs)) {
const d = devs.find(item => item.id === discordId);
if (d) targetName = d.name;
} else if (devs[discordId]) {
targetName = devs[discordId].name;
}
}
if (!targetName) return "default";
>>>>>>> 5ab15b59 (fix bugs)
let webDevs = await navigator.mediaDevices.enumerateDevices();
// trigger permissions if empty labels
if (webDevs.some(d => d.kind === "audioinput" && !d.label)) {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(t => t.stop());
@@ -143,9 +156,15 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
let match = webDevs.find(d => d.kind === "audioinput" && d.deviceId === discordId);
if (!match) {
<<<<<<< HEAD
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
const normSelected = normalize(selected.name);
=======
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
const normSelected = normalize(targetName);
>>>>>>> 5ab15b59 (fix bugs)
match = webDevs.find(d => {
if (d.kind !== "audioinput" || !d.label) return false;
const normLabel = normalize(d.label);
@@ -154,11 +173,11 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
}
if (match) {
console.log(`[VoiceDictation] Mapped Discord device "${selected.name}" to WebAudio deviceId "${match.deviceId}"`);
showToast(`Dictation: Using mic "${match.label || selected.name}"`, Toasts.Type.SUCCESS);
console.log(`[VoiceDictation] Mapped Discord device "${targetName}" to WebAudio deviceId "${match.deviceId}"`);
showToast(`Dictation: Using mic "${match.label || targetName}"`, Toasts.Type.SUCCESS);
return match.deviceId;
} else {
showToast(`Dictation: Could not map "${selected.name}", using default`, Toasts.Type.FAILURE);
showToast(`Dictation: Could not map "${targetName}", using default`, Toasts.Type.FAILURE);
}
} catch (err) {
console.error("[VoiceDictation] Error mapping device ID:", err);
@@ -167,14 +186,12 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
return "default";
}
// Open microphone
let stream: MediaStream;
try {
const discordDeviceId = MediaEngineStore.getInputDeviceId();
const realDeviceId = await getRealInputDeviceId(discordDeviceId);
try {
// Premier essai : avec le deviceId spécifique (fonctionne si permission ok)
stream = await navigator.mediaDevices.getUserMedia({
audio: realDeviceId && realDeviceId !== "default"
? { deviceId: { exact: realDeviceId } }
@@ -182,7 +199,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
});
} catch (firstErr: any) {
if (firstErr.name === "NotAllowedError" || firstErr.name === "PermissionDeniedError") {
// Fallback: permission pas encore accordée, demander sans deviceId
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
} else {
throw firstErr;
@@ -198,7 +214,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
return;
}
// Choose audio format
const mimeType = ["audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/mp4"]
.find(m => MediaRecorder.isTypeSupported(m)) ?? "";
@@ -213,7 +228,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
recorder.start();
setRecording(true);
// Flush and transcribe at regular intervals
const chunkMs = (settings.store.chunkSeconds ?? 5) * 1000;
timerRef.current = setInterval(() => flushAndTranscribe(), chunkMs);
}
@@ -221,7 +235,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
async function flushAndTranscribe() {
if (!recorderRef.current || recorderRef.current.state !== "recording") return;
// Stop briefly to get a complete blob
recorderRef.current.stop();
await new Promise<void>(resolve => {
@@ -232,7 +245,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
chunksRef.current = [];
if (chunks.length === 0 || !activeRef.current) {
// Restart if still active
if (activeRef.current && streamRef.current) restartRecorder();
return;
}
@@ -240,36 +252,26 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
const mimeType = recorderRef.current?.mimeType || "audio/webm";
const blob = new Blob(chunks, { type: mimeType });
// Debug log
console.log("[VoiceDictation] Blob size:", blob.size);
if (blob.size < 500) {
if (activeRef.current) restartRecorder();
return; // Silence / too short
return;
}
setProcessing(true);
try {
const text = await transcribe(blob);
// Debug log
console.log("[VoiceDictation] Transcribed text:", text);
if (text && text.length > 0) {
// Filter common Whisper hallucinations (silence → phantom text)
const t = text.trim();
const isHallucination =
// Simple exact patterns
/^(merci|thanks?|thank you|music|♪|🎵|\.\.\.|\.\s*)+$/i.test(t) ||
// Subtitling / subtitles
/sous[- ]?titr/i.test(t) ||
// Radio-Canada, SRC, etc.
/radio[- ]?canada|société radio/i.test(t) ||
// "Thanks for watching", etc.
/merci .*(regard|écouter|suivi)|thanks? .*watch/i.test(t) ||
// "Transcription by...", "Transcribed by..."
/transcri(ption|t)\s*(par|by)/i.test(t) ||
// Repetitive text (same word/syllable 3+ times)
/^(.{1,15})\1{2,}$/i.test(t.replace(/\s+/g, "")) ||
// Too short and punctuation only
/^[\s.,!?…\-–—]+$/.test(t);
if (!isHallucination) insertText(text + " ");
}
@@ -324,7 +326,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
function toggle() {
if (recording) {
// Flush immediate transcription on stop
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
@@ -365,4 +366,4 @@ export default definePlugin({
icon: DictationIcon as any,
render: VoiceDictationButton,
},
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1 @@
declare module "*.png";
@@ -0,0 +1,9 @@
import "./style.css";
export const Box = () => {
return (
<div className="box">
<div className="pill" />
</div>
);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

@@ -0,0 +1,9 @@
.box {
display: none !important;
background: transparent !important;
}
.box .pill {
display: none !important;
}
@@ -1,21 +1,3 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginNative } from "@utils/types";
import { Button, MediaEngineStore, showToast, Toasts, useState } from "@webpack/common";
@@ -1,21 +1,4 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { MouseEvent } from "react";
import { useTimer } from "@utils/react";
import { cl, VoiceMessage } from "..";
@@ -32,12 +15,14 @@ export type VoicePreviewOptions = {
src?: string;
waveform: string;
recording?: boolean;
onDownload?: () => void;
};
export const VoicePreview = ({
src,
waveform,
recording,
onDownload,
}: VoicePreviewOptions) => {
const durationMs = useTimer({
deps: [recording]
@@ -46,8 +31,26 @@ export const VoicePreview = ({
const durationSeconds = recording ? Math.floor(durationMs / 1000) : 0;
const durationDisplay = Math.floor(durationSeconds / 60) + ":" + (durationSeconds % 60).toString().padStart(2, "0");
const handleClick = (event: MouseEvent<HTMLDivElement>) => {
if (!onDownload) return;
const target = event.target as HTMLElement;
const link = target.closest('a[href^="blob:"]') as HTMLAnchorElement | null;
if (link) {
event.preventDefault();
event.stopPropagation();
onDownload();
}
};
if (src && !recording)
return <VoiceMessage key={src} src={src} waveform={waveform} />;
return (
<div className={cl("preview", "preview-playback")} onClick={handleClick}>
<div className={cl("preview-message")}>
<VoiceMessage key={src} src={src} waveform={waveform} />
</div>
</div>
);
return (
<div className={cl("preview", recording ? "preview-recording" : [])}>
@@ -1,21 +1,3 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Button, MediaEngineStore, useState } from "@webpack/common";
import { settings, type VoiceRecorder } from "..";
@@ -72,21 +54,8 @@ export const VoiceRecorderWeb: VoiceRecorder = ({ setAudioBlob, onRecordingChang
}
return (
<>
<Button onClick={toggleRecording}>
{recording ? "Stop" : "Start"} recording
</Button>
<Button
disabled={!recording}
onClick={() => {
setPaused(!paused);
if (paused) recorder?.resume();
else recorder?.pause();
}}
>
{paused ? "Resume" : "Pause"} recording
</Button>
</>
<Button onClick={toggleRecording}>
{recording ? "Stop" : "Start"} recording
</Button>
);
};
+76 -28
View File
@@ -1,22 +1,7 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./styles.css";
import "./FIGMAUI/style.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
@@ -24,16 +9,15 @@ import { Card } from "@components/Card";
import { Microphone } from "@components/Icons";
import { Link } from "@components/Link";
import { Paragraph } from "@components/Paragraph";
import { lastState as silentMessageEnabled } from "@plugins/silentMessageToggle";
import { lastState as silentMessageEnabled } from "@nightcordplugins/silentMessageToggle";
import { Devs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { Margins } from "@utils/margins";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { chooseFile } from "@utils/web";
import { RenderModalProps } from "@vencord/discord-types";
import { CloudUploadPlatform } from "@vencord/discord-types/enums";
import { Button, CloudUploader, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, Modal, openModal, PendingReplyStore, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { Button, CloudUploader, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, Modal, openModal, PendingReplyStore, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState, useRef } from "@webpack/common";
import { ComponentType } from "react";
import { VoiceRecorderDesktop } from "./components/DesktopRecorder";
@@ -79,7 +63,9 @@ export const settings = definePluginSettings({
});
const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (!props || !props.channel) return;
if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return;
if (!Menu || !Menu.MenuItem) return;
children.push(
<Menu.MenuItem
@@ -205,7 +191,7 @@ function useObjectUrl() {
return [url, setWithFree] as const;
}
function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
function VoiceMessageModal({ modalProps }: { modalProps: any; }) {
const [isRecording, setRecording] = useState(false);
const [blob, setBlob] = useState<Blob>();
const [blobUrl, setBlobUrl] = useObjectUrl();
@@ -234,9 +220,44 @@ function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
const isUnsupportedFormat = blob && (!blob.type.startsWith("audio/ogg") || blob.type.includes("codecs") && !blob.type.includes("opus"));
const downloadBlob = () => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = "voice-message.ogg";
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
setTimeout(() => URL.revokeObjectURL(url), 1000);
};
useEffect(() => {
const handler = (e: Event) => {
try {
const target = e.target as HTMLElement | null;
if (!target) return;
if (!target.closest || !document.body.querySelector) return;
const modalRoot = target.closest('.vc-vmsg-figma-ui') || target.closest('.vc-vmsg-figma-ui-content');
if (!modalRoot) return;
const anchor = target.closest('a[href^="blob:"]') as HTMLAnchorElement | null;
if (anchor) {
e.preventDefault();
e.stopPropagation();
downloadBlob();
}
} catch (err) {
}
};
document.addEventListener('click', handler, true);
return () => document.removeEventListener('click', handler, true);
}, [blob]);
return (
<Modal
{...modalProps}
className={cl("modal")}
title="Record Voice Message"
actions={[{
text: "Send",
@@ -249,8 +270,10 @@ function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
disabled: !blob
}]}
>
<div className={cl("buttons")}>
<VoiceRecorder
<div className={cl("figma-ui")}>
<div className={cl("figma-ui-content")}>
<div className={cl("buttons")}>
<VoiceRecorder
setAudioBlob={blob => {
setBlob(blob);
setBlobUrl(blob);
@@ -275,11 +298,34 @@ function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
{metaError
? <Paragraph className={cl("error")}>Failed to parse selected audio file: {metaError.message}</Paragraph>
: (
<VoicePreview
src={blobUrl}
waveform={meta.waveform}
recording={isRecording}
/>
<>
<div className={cl("preview-container")}>
<VoicePreview
src={blobUrl}
waveform={meta.waveform}
recording={isRecording}
onDownload={downloadBlob}
/>
</div>
{blob && (
<div className={cl("send-row")}>
<Button
onClick={() => {
try {
sendAudio(blob, meta ?? EMPTY_META);
modalProps.onClose();
showToast("Now sending voice message... Please be patient", Toasts.Type.MESSAGE);
} catch (e) {
showToast("Failed to send voice message", Toasts.Type.FAILURE);
}
}}
disabled={!blob}
>
Send
</Button>
</div>
)}
</>
)}
{isUnsupportedFormat && (
@@ -291,6 +337,8 @@ function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
</Forms.FormText>
</Card>
)}
</div>
</div>
</Modal>
);
}
@@ -1,9 +1,3 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app } from "electron";
import { readFile, rm } from "fs/promises";
import { basename, normalize } from "path";
+149 -10
View File
@@ -1,23 +1,153 @@
.vc-vmsg-modal {
padding: 1em;
padding: 0;
margin: 0;
background: transparent;
box-shadow: none;
border: none;
min-width: auto;
}
.vc-vmsg-buttons {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5em;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75em;
margin-bottom: 1em;
}
.vc-vmsg-figma-ui {
position: relative;
display: inline-flex;
justify-content: center;
width: auto;
max-width: min(520px, 100%);
margin: 0 auto;
padding: 0;
background: transparent;
}
.vc-vmsg-figma-ui-content {
position: relative;
z-index: 1;
width: auto;
max-width: 520px;
padding: 28px 24px 26px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 18px;
align-items: center;
text-align: center;
background: #1f2126;
border-radius: 28px;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.1);
}
.vc-vmsg-preview-container {
position: relative;
width: 100%;
margin-top: 18px;
display: flex;
justify-content: center;
}
.vc-vmsg-figma-ui-content .buttons {
display: flex;
gap: 12px;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
margin-top: 0;
width: 100%;
max-width: 460px;
}
.vc-vmsg-figma-ui-content .buttons button,
.vc-vmsg-figma-ui-content .buttons [role="button"] {
height: 44px !important;
flex: 1;
min-width: 150px !important;
max-width: 230px !important;
padding: 0 26px !important;
border-radius: 14px !important;
background: #4f535b !important;
color: #ebedef !important;
font-weight: 700 !important;
font-size: 14px !important;
letter-spacing: 0.02em !important;
border: 1px solid rgba(255,255,255,0.09) !important;
box-shadow: none !important;
}
.vc-vmsg-figma-ui-content .buttons button:hover,
.vc-vmsg-figma-ui-content .buttons [role="button"]:hover {
background: #5f646d !important;
}
.vc-vmsg-figma-ui-content .buttons button[disabled],
.vc-vmsg-figma-ui-content .buttons [role="button"][aria-disabled="true"] {
opacity: 0.55 !important;
cursor: not-allowed !important;
}
.vc-vmsg-preview {
color: var(--text-default);
border-radius: 24px;
background-color: var(--background-base-lower);
background: transparent;
border: none;
box-shadow: none;
border-radius: 0;
padding: 0;
min-height: auto;
position: relative;
display: flex;
align-items: center;
padding: 0 16px;
height: 48px;
justify-content: center;
width: min(100%, 520px);
margin: 0 auto;
}
.vc-vmsg-preview-playback {
position: relative;
width: 100%;
}
.vc-vmsg-preview-message {
width: 100%;
min-width: 0;
}
.vc-vmsg-download-icon {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
display: inline-grid;
place-items: center;
width: 36px;
height: 36px;
background: rgba(255,255,255,0.08);
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.08);
color: var(--text-normal);
cursor: pointer;
}
.vc-vmsg-download-icon svg {
display: block;
}
.vc-vmsg-download-icon:hover {
background: rgba(255,255,255,0.12);
}
.vc-vmsg-download-wrap {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
z-index: 2;
}
.vc-vmsg-preview-indicator {
@@ -37,18 +167,27 @@
margin: 0 0.5em;
font-size: 80%;
/* monospace so different digits have same size */
font-family: var(--font-code);
}
.vc-vmsg-preview-label {
opacity: 0.5;
letter-spacing: 0.125em;
opacity: 0.6;
letter-spacing: 0.12em;
font-weight: 600;
flex: 1;
text-align: center;
}
.vc-vmsg-figma-ui-content h4,
.vc-vmsg-figma-ui-content h5,
.vc-vmsg-figma-ui-content .formTitle {
width: 100%;
margin: 0;
text-align: center;
color: #ecedef;
}
.vc-vmsg-error {
color: var(--text-feedback-critical, #FF5C5C);
}