825160ee46
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.
Highlights:
- All 17 Tauri release commits ported (audio fixes, custom notification
sound, Discord-style chat UX, profile banner, changelog page,
Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
fix).
- Native napi-rs audio-loopback addon with WASAPI process-loopback:
* EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
never hear themselves echoed back through the capture.
* INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
picked window's audio is captured, not the whole OS mixer
(Discord parity).
* HWND -> PID resolution via Win32 GetWindowThreadProcessId.
- Discord-style screen-source picker (thumbnail grid, screens vs
apps tabs, live-refreshing thumbnails).
- Hash routing fix for packaged builds (file:// can't resolve
BrowserRouter paths).
- Tauri sources removed (apps/desktop/src-tauri).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
5.0 KiB
TypeScript
121 lines
5.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
import { PollIcon, PlusIcon, SpinnerIcon, TrashIcon } from './icons';
|
|
import { Modal } from './Modal';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
sending: boolean;
|
|
error: string | null;
|
|
onClose: () => void;
|
|
onSubmit: (question: string, options: string[]) => Promise<void>;
|
|
}
|
|
|
|
export function PollComposerDialog({ open, sending, error, onClose, onSubmit }: Props) {
|
|
const [question, setQuestion] = useState('');
|
|
const [options, setOptions] = useState(['', '']);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
setQuestion('');
|
|
setOptions(['', '']);
|
|
}, [open]);
|
|
|
|
const filledOptions = options.map((option) => option.trim()).filter(Boolean);
|
|
const canSubmit = question.trim().length > 0 && filledOptions.length >= 2 && !sending;
|
|
|
|
return (
|
|
<Modal open={open} title="Umfrage erstellen" onClose={onClose} size="md">
|
|
<form
|
|
className="flex flex-col gap-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
if (!canSubmit) return;
|
|
void onSubmit(question, options);
|
|
}}
|
|
>
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="font-semibold text-fg">Frage</span>
|
|
<input
|
|
autoFocus
|
|
value={question}
|
|
onChange={(e) => setQuestion(e.target.value)}
|
|
maxLength={140}
|
|
placeholder="Worüber sollen wir abstimmen?"
|
|
className="rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm text-fg outline-none transition placeholder:text-fg-muted focus:border-accent/60 focus:ring-2 focus:ring-accent/20"
|
|
/>
|
|
</label>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm font-semibold text-fg">Antworten</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOptions((prev) => [...prev, ''])}
|
|
disabled={options.length >= 10}
|
|
className="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-fg-muted transition hover:bg-surface-2 hover:text-fg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed disabled:opacity-45"
|
|
aria-label="Antwort hinzufügen"
|
|
title="Antwort hinzufügen"
|
|
>
|
|
<PlusIcon className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
{options.map((option, idx) => (
|
|
<div key={idx} className="flex items-center gap-2">
|
|
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-accent/10 text-xs font-semibold text-accent">
|
|
{idx + 1}
|
|
</span>
|
|
<input
|
|
value={option}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
setOptions((prev) =>
|
|
prev.map((item, itemIdx) => (itemIdx === idx ? value : item)),
|
|
);
|
|
}}
|
|
maxLength={80}
|
|
placeholder={'Antwort ' + (idx + 1)}
|
|
className="min-w-0 flex-1 rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm text-fg outline-none transition placeholder:text-fg-muted focus:border-accent/60 focus:ring-2 focus:ring-accent/20"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOptions((prev) => prev.filter((_, itemIdx) => itemIdx !== idx))}
|
|
disabled={options.length <= 2}
|
|
aria-label="Antwort entfernen"
|
|
title="Antwort entfernen"
|
|
className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-lg text-fg-muted transition hover:bg-rose-500/15 hover:text-rose-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-500/30 disabled:cursor-not-allowed disabled:opacity-35"
|
|
>
|
|
<TrashIcon className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="rounded-lg border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-xs text-rose-600 dark:text-rose-200">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-2 pt-1">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="cursor-pointer rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm text-fg transition hover:bg-surface-3"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={!canSubmit}
|
|
className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-accent px-3 py-2 text-sm font-semibold text-accent-fg transition hover:brightness-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 disabled:cursor-not-allowed disabled:opacity-45"
|
|
>
|
|
{sending ? <SpinnerIcon className="h-4 w-4" /> : <PollIcon className="h-4 w-4" />}
|
|
<span>Senden</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
);
|
|
}
|