feat: text to speech and animation for zap alerts
This commit is contained in:
@ -1,14 +1,149 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import "./widgets.css";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
|
||||
import { NostrPrefix, createNostrLink } from "@snort/system";
|
||||
import Copy from "element/copy";
|
||||
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
|
||||
import { getVoices, speak, toTextToSpeechParams } from "text2speech";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { eventToLink, hexToBech32 } from "utils";
|
||||
import { ZapAlertItem } from "./widgets/zaps";
|
||||
import { TopZappersWidget } from "./widgets/top-zappers";
|
||||
import { Views } from "./widgets/views";
|
||||
import groupBy from "lodash/groupBy";
|
||||
|
||||
interface ZapAlertConfigurationProps {
|
||||
npub: string;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) {
|
||||
const login = useLogin();
|
||||
const { formatMessage, formatDisplayName } = useIntl();
|
||||
|
||||
const [testText, setTestText] = useState("");
|
||||
const [textToSpeech, setTextToSpeech] = useState<boolean>(false);
|
||||
const [voice, setVoice] = useState<string | null>(null);
|
||||
const [minSatsForTextToSpeech, setMinSatsForTextToSpeech] = useState<string>("21");
|
||||
|
||||
// Google propietary voices are not available on OBS browser
|
||||
const voices = getVoices().filter(v => !v.name.includes("Google"));
|
||||
const groupedVoices = useMemo(() => {
|
||||
return groupBy(voices, v => v.lang);
|
||||
}, [voices]);
|
||||
const languages = useMemo(() => {
|
||||
return Object.keys(groupedVoices).sort();
|
||||
}, [groupedVoices]);
|
||||
const selectedVoice = useMemo(() => {
|
||||
return voices.find(v => v.voiceURI === voice);
|
||||
}, [voice]);
|
||||
|
||||
const isTextToSpeechSupported = "speechSynthesis" in window;
|
||||
const isTextToSpeechEnabled = voices.length > 0 && textToSpeech;
|
||||
|
||||
const query = useMemo(() => {
|
||||
const params = toTextToSpeechParams({
|
||||
voiceURI: voice,
|
||||
minSats: voice ? Number(minSatsForTextToSpeech) : null,
|
||||
});
|
||||
const queryParams = params.toString();
|
||||
return queryParams.length > 0 ? `?${queryParams}` : "";
|
||||
}, [voice, minSatsForTextToSpeech]);
|
||||
|
||||
function testVoice() {
|
||||
if (selectedVoice) {
|
||||
speak(selectedVoice, testText);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Zap Alert" />
|
||||
</h3>
|
||||
<Copy text={`${baseUrl}/alert/${npub}/zaps${query}`} />
|
||||
<ZapAlertItem
|
||||
item={{
|
||||
id: "",
|
||||
valid: true,
|
||||
content: testText,
|
||||
zapService: "",
|
||||
anonZap: false,
|
||||
errors: [],
|
||||
sender: login?.pubkey,
|
||||
amount: 1_000_000,
|
||||
}}
|
||||
/>
|
||||
<div className="text-to-speech-settings">
|
||||
<div
|
||||
className="paper"
|
||||
onClick={() => setTextToSpeech(!textToSpeech)}
|
||||
style={{ cursor: isTextToSpeechSupported ? "pointer" : "not-allowed" }}>
|
||||
<input
|
||||
disabled={!isTextToSpeechSupported}
|
||||
type="checkbox"
|
||||
checked={textToSpeech}
|
||||
onChange={ev => setTextToSpeech(ev.target.checked)}
|
||||
/>
|
||||
<FormattedMessage defaultMessage="Enable text to speech" />
|
||||
</div>
|
||||
{isTextToSpeechEnabled && (
|
||||
<>
|
||||
<div className="paper labeled-input">
|
||||
<label htmlFor="minimum-sats">
|
||||
<FormattedMessage defaultMessage="Minimum amount for text to speech" />
|
||||
</label>
|
||||
<input
|
||||
id="minimum-sats"
|
||||
type="number"
|
||||
min="1"
|
||||
value={minSatsForTextToSpeech}
|
||||
onChange={ev => setMinSatsForTextToSpeech(ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="paper labeled-input">
|
||||
<label htmlFor="voice-selector">
|
||||
<FormattedMessage defaultMessage="Voice" />
|
||||
</label>
|
||||
<select id="voice-selector" onChange={ev => setVoice(ev.target.value)}>
|
||||
<option value="">
|
||||
<FormattedMessage defaultMessage="Select voice..." />
|
||||
</option>
|
||||
{languages.map(l => (
|
||||
<optgroup label={formatDisplayName(l, { type: "language" })}>
|
||||
{groupedVoices[l].map(v => (
|
||||
<option value={v.voiceURI}>{v.name}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{voice && (
|
||||
<>
|
||||
<div className="paper labeled-input">
|
||||
<label htmlFor="zap-alert-text">
|
||||
<FormattedMessage defaultMessage="Zap message" />
|
||||
</label>
|
||||
<textarea
|
||||
id="zap-alert-text"
|
||||
placeholder={formatMessage({ defaultMessage: "Insert text to speak" })}
|
||||
value={testText}
|
||||
onChange={ev => setTestText(ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button disabled={testText.length === 0} className="btn" onClick={testVoice}>
|
||||
<FormattedMessage defaultMessage="Test voice" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function WidgetsPage() {
|
||||
const login = useLogin();
|
||||
@ -18,6 +153,7 @@ export function WidgetsPage() {
|
||||
const npub = hexToBech32("npub", login?.pubkey);
|
||||
|
||||
const baseUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
return (
|
||||
<div className="widgets g8">
|
||||
<div className="flex f-col g8">
|
||||
@ -27,21 +163,7 @@ export function WidgetsPage() {
|
||||
<Copy text={`${baseUrl}/chat/${npub}`} />
|
||||
</div>
|
||||
<div className="flex f-col g8">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Zap Alert" />
|
||||
</h3>
|
||||
<Copy text={`${baseUrl}/alert/${npub}/zaps`} />
|
||||
<ZapAlertItem
|
||||
item={{
|
||||
id: "",
|
||||
valid: true,
|
||||
zapService: "",
|
||||
anonZap: false,
|
||||
errors: [],
|
||||
sender: login?.pubkey,
|
||||
amount: 1_000_000,
|
||||
}}
|
||||
/>
|
||||
<ZapAlertConfiguration npub={npub} baseUrl={baseUrl} />
|
||||
</div>
|
||||
<div className="flex f-col g8">
|
||||
<h3>
|
||||
|
Reference in New Issue
Block a user