feat: nests chat / speak
This commit is contained in:
@ -463,6 +463,14 @@
|
||||
<path d="M14 3C14 2.44772 14.4477 2 15 2H21C21.5523 2 22 2.44772 22 3V9C22 9.55229 21.5523 10 21 10C20.4477 10 20 9.55229 20 9V5.41421L14.7071 10.7071C14.3166 11.0976 13.6834 11.0976 13.2929 10.7071C12.9024 10.3166 12.9024 9.68342 13.2929 9.29289L18.5858 4H15C14.4477 4 14 3.55228 14 3Z" fill="currentColor" />
|
||||
<path d="M5.41421 20L10.7071 14.7071C11.0976 14.3166 11.0976 13.6834 10.7071 13.2929C10.3166 12.9024 9.68342 12.9024 9.29289 13.2929L4 18.5858L4 15C4 14.4477 3.55229 14 3 14C2.44772 14 2 14.4477 2 15V21C2 21.5523 2.44772 22 3 22H9C9.55228 22 10 21.5523 10 21C10 20.4477 9.55228 20 9 20H5.41421Z" fill="currentColor" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="mic" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 13.4375C11.0771 13.4363 12.1097 13.0078 12.8713 12.2463C13.6328 11.4847 14.0613 10.4521 14.0625 9.375V5C14.0625 3.92256 13.6345 2.88925 12.8726 2.12738C12.1108 1.36551 11.0774 0.9375 10 0.9375C8.92256 0.9375 7.88925 1.36551 7.12738 2.12738C6.36551 2.88925 5.9375 3.92256 5.9375 5V9.375C5.93874 10.4521 6.36715 11.4847 7.12875 12.2463C7.89035 13.0078 8.92294 13.4363 10 13.4375ZM7.8125 5C7.8125 4.41984 8.04297 3.86344 8.4532 3.4532C8.86344 3.04297 9.41984 2.8125 10 2.8125C10.5802 2.8125 11.1366 3.04297 11.5468 3.4532C11.957 3.86344 12.1875 4.41984 12.1875 5V9.375C12.1875 9.95516 11.957 10.5116 11.5468 10.9218C11.1366 11.332 10.5802 11.5625 10 11.5625C9.41984 11.5625 8.86344 11.332 8.4532 10.9218C8.04297 10.5116 7.8125 9.95516 7.8125 9.375V5ZM10.9375 16.5016V18.125C10.9375 18.3736 10.8387 18.6121 10.6629 18.7879C10.4871 18.9637 10.2486 19.0625 10 19.0625C9.75136 19.0625 9.5129 18.9637 9.33709 18.7879C9.16127 18.6121 9.0625 18.3736 9.0625 18.125V16.5016C7.3344 16.2719 5.74838 15.4229 4.59892 14.1122C3.44947 12.8016 2.81471 11.1183 2.8125 9.375C2.8125 9.12636 2.91127 8.8879 3.08709 8.71209C3.2629 8.53627 3.50136 8.4375 3.75 8.4375C3.99864 8.4375 4.2371 8.53627 4.41291 8.71209C4.58873 8.8879 4.6875 9.12636 4.6875 9.375C4.6875 10.784 5.24721 12.1352 6.2435 13.1315C7.23978 14.1278 8.59104 14.6875 10 14.6875C11.409 14.6875 12.7602 14.1278 13.7565 13.1315C14.7528 12.1352 15.3125 10.784 15.3125 9.375C15.3125 9.12636 15.4113 8.8879 15.5871 8.71209C15.7629 8.53627 16.0014 8.4375 16.25 8.4375C16.4986 8.4375 16.7371 8.53627 16.9129 8.71209C17.0887 8.8879 17.1875 9.12636 17.1875 9.375C17.1853 11.1183 16.5505 12.8016 15.4011 14.1122C14.2516 15.4229 12.6656 16.2719 10.9375 16.5016Z" fill="currentColor"/>
|
||||
</symbol>
|
||||
<symbol id="mic-off" viewBox="0 0 24 30" fill="none">
|
||||
<path d="M3.10989 2.99125C2.97816 2.84276 2.81827 2.72189 2.63949 2.63565C2.46071 2.54942 2.26658 2.49952 2.06837 2.48885C1.87016 2.47819 1.67181 2.50697 1.48481 2.57353C1.2978 2.6401 1.12587 2.74311 0.978971 2.87661C0.832073 3.01011 0.713132 3.17143 0.629043 3.35124C0.544953 3.53104 0.497387 3.72575 0.489101 3.92407C0.480814 4.1224 0.511973 4.32039 0.580772 4.50658C0.64957 4.69278 0.754638 4.86346 0.889888 5.00875L5.49989 10.08V14C5.49894 15.0718 5.76306 16.1273 6.26872 17.0723C6.77439 18.0174 7.50591 18.8227 8.39814 19.4166C9.29038 20.0105 10.3156 20.3746 11.3826 20.4764C12.4496 20.5782 13.5252 20.4145 14.5136 20L15.9211 21.5487C14.7105 22.1791 13.3648 22.5055 11.9999 22.5C9.74626 22.4977 7.5856 21.6014 5.99204 20.0078C4.39848 18.4143 3.5022 16.2536 3.49989 14C3.49989 13.6022 3.34185 13.2206 3.06055 12.9393C2.77924 12.658 2.39771 12.5 1.99989 12.5C1.60206 12.5 1.22053 12.658 0.939228 12.9393C0.657924 13.2206 0.499888 13.6022 0.499888 14C0.503423 16.7893 1.51905 19.4825 3.35817 21.5795C5.19729 23.6766 7.73494 25.035 10.4999 25.4025V28C10.4999 28.3978 10.6579 28.7794 10.9392 29.0607C11.2205 29.342 11.6021 29.5 11.9999 29.5C12.3977 29.5 12.7792 29.342 13.0605 29.0607C13.3419 28.7794 13.4999 28.3978 13.4999 28V25.4037C15.0922 25.2004 16.6228 24.66 17.9899 23.8187L20.8899 27.0087C21.1588 27.2977 21.5308 27.4689 21.9252 27.4854C22.3195 27.5019 22.7045 27.3622 22.9966 27.0968C23.2887 26.8313 23.4644 26.4614 23.4856 26.0673C23.5068 25.6731 23.3718 25.2865 23.1099 24.9912L3.10989 2.99125ZM11.9999 17.5C11.0716 17.5 10.1814 17.1313 9.52501 16.4749C8.86864 15.8185 8.49989 14.9283 8.49989 14V13.375L12.2374 17.4862C12.1586 17.5 12.0799 17.5 11.9999 17.5ZM7.33364 4.65875C7.18979 4.52338 7.0741 4.36094 6.9932 4.18075C6.9123 4.00055 6.86778 3.80616 6.86221 3.60871C6.85663 3.41127 6.89011 3.21467 6.96071 3.0302C7.03132 2.84572 7.13766 2.67701 7.27364 2.53375C8.16735 1.58718 9.3247 0.930779 10.5958 0.649565C11.8668 0.368351 13.1931 0.475282 14.4027 0.956509C15.6123 1.43774 16.6495 2.27108 17.38 3.34861C18.1105 4.42614 18.5007 5.69819 18.4999 7V13.0675C18.4999 13.4653 18.3419 13.8469 18.0605 14.1282C17.7792 14.4095 17.3977 14.5675 16.9999 14.5675C16.6021 14.5675 16.2205 14.4095 15.9392 14.1282C15.6579 13.8469 15.4999 13.4653 15.4999 13.0675V7C15.4998 6.29921 15.2894 5.61457 14.8959 5.03471C14.5024 4.45486 13.9438 4.00649 13.2926 3.74766C12.6413 3.48884 11.9274 3.43147 11.2432 3.58298C10.5589 3.7345 9.93597 4.08792 9.45489 4.5975C9.31967 4.74087 9.15752 4.85619 8.97771 4.93687C8.7979 5.01755 8.60395 5.062 8.40696 5.06769C8.20996 5.07337 8.01377 5.04019 7.82961 4.97002C7.64544 4.89985 7.47691 4.79408 7.33364 4.65875ZM19.8749 17.1975C20.2886 16.1823 20.5009 15.0963 20.4999 14C20.4999 13.6022 20.6579 13.2206 20.9392 12.9393C21.2205 12.658 21.6021 12.5 21.9999 12.5C22.3977 12.5 22.7792 12.658 23.0605 12.9393C23.3419 13.2206 23.4999 13.6022 23.4999 14C23.5023 15.4831 23.2161 16.9524 22.6574 18.3262C22.4983 18.6801 22.2082 18.9585 21.8482 19.1031C21.4881 19.2476 21.0861 19.247 20.7264 19.1014C20.3668 18.9558 20.0776 18.6766 19.9195 18.3222C19.7614 17.9679 19.7468 17.5661 19.8786 17.2012L19.8749 17.1975Z" fill="currentColor"/>
|
||||
</symbol>
|
||||
<symbol id="hand" viewBox="0 0 24 30" fill="none">
|
||||
<path d="M19.5 9.49996C19.1627 9.4993 18.8267 9.5413 18.5 9.62496V6.49996C18.5006 5.85364 18.3445 5.2168 18.0452 4.64397C17.7458 4.07114 17.3121 3.57937 16.7812 3.21077C16.2503 2.84216 15.638 2.6077 14.9967 2.52745C14.3553 2.4472 13.7041 2.52355 13.0988 2.74996C12.7035 1.9353 12.0435 1.27886 11.2267 0.887983C10.4099 0.497104 9.48469 0.394917 8.60229 0.598133C7.7199 0.80135 6.93256 1.29794 6.36903 2.00671C5.8055 2.71547 5.49913 3.59447 5.5 4.49996V4.62496C4.90875 4.4723 4.2904 4.45704 3.69233 4.58034C3.09427 4.70363 2.53237 4.96222 2.04971 5.33629C1.56705 5.71035 1.17644 6.18995 0.907819 6.73833C0.639196 7.28672 0.499693 7.88932 0.500001 8.49996V18C0.500001 21.05 1.7116 23.975 3.86827 26.1317C6.02494 28.2884 8.95001 29.5 12 29.5C15.05 29.5 17.9751 28.2884 20.1317 26.1317C22.2884 23.975 23.5 21.05 23.5 18V13.5C23.5 12.4391 23.0786 11.4217 22.3284 10.6715C21.5783 9.92139 20.5609 9.49996 19.5 9.49996ZM20.5 18C20.5 20.2543 19.6045 22.4163 18.0104 24.0104C16.4163 25.6044 14.2543 26.5 12 26.5C9.74566 26.5 7.58365 25.6044 5.98959 24.0104C4.39553 22.4163 3.5 20.2543 3.5 18V8.49996C3.5 8.23475 3.60536 7.98039 3.79289 7.79286C3.98043 7.60532 4.23478 7.49996 4.5 7.49996C4.76522 7.49996 5.01957 7.60532 5.20711 7.79286C5.39464 7.98039 5.5 8.23475 5.5 8.49996V14C5.5 14.3978 5.65804 14.7793 5.93934 15.0606C6.22064 15.3419 6.60218 15.5 7 15.5C7.39783 15.5 7.77936 15.3419 8.06066 15.0606C8.34197 14.7793 8.5 14.3978 8.5 14V4.49996C8.5 4.23475 8.60536 3.98039 8.79289 3.79286C8.98043 3.60532 9.23478 3.49996 9.5 3.49996C9.76522 3.49996 10.0196 3.60532 10.2071 3.79286C10.3946 3.98039 10.5 4.23475 10.5 4.49996V13C10.5 13.3978 10.658 13.7793 10.9393 14.0606C11.2206 14.3419 11.6022 14.5 12 14.5C12.3978 14.5 12.7794 14.3419 13.0607 14.0606C13.342 13.7793 13.5 13.3978 13.5 13V6.49996C13.5 6.23475 13.6054 5.98039 13.7929 5.79286C13.9804 5.60532 14.2348 5.49996 14.5 5.49996C14.7652 5.49996 15.0196 5.60532 15.2071 5.79286C15.3946 5.98039 15.5 6.23475 15.5 6.49996V14.675C14.0773 15.0144 12.8103 15.823 11.9033 16.9705C10.9962 18.1179 10.5019 19.5373 10.5 21C10.5 21.3978 10.658 21.7793 10.9393 22.0606C11.2206 22.3419 11.6022 22.5 12 22.5C12.3978 22.5 12.7794 22.3419 13.0607 22.0606C13.342 21.7793 13.5 21.3978 13.5 21C13.5 20.0717 13.8687 19.1815 14.5251 18.5251C15.1815 17.8687 16.0717 17.5 17 17.5C17.3978 17.5 17.7794 17.3419 18.0607 17.0606C18.342 16.7793 18.5 16.3978 18.5 16V13.5C18.5 13.2347 18.6054 12.9804 18.7929 12.7929C18.9804 12.6053 19.2348 12.5 19.5 12.5C19.7652 12.5 20.0196 12.6053 20.2071 12.7929C20.3946 12.9804 20.5 13.2347 20.5 13.5V18Z" fill="currentColor"/>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 139 KiB |
77
packages/app/src/Components/LiveStream/VU.tsx
Normal file
77
packages/app/src/Components/LiveStream/VU.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function VuBar({
|
||||
track,
|
||||
full,
|
||||
width,
|
||||
height,
|
||||
className,
|
||||
}: {
|
||||
track?: MediaStreamTrack;
|
||||
full?: boolean;
|
||||
height?: number;
|
||||
width?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref && track) {
|
||||
const audioContext = new AudioContext();
|
||||
|
||||
const trackClone = track;
|
||||
const mediaStreamSource = audioContext.createMediaStreamSource(new MediaStream([trackClone]));
|
||||
const analyser = audioContext.createAnalyser();
|
||||
const minVU = -60;
|
||||
const maxVU = 0;
|
||||
const minFreq = 50;
|
||||
const maxFreq = 7_000;
|
||||
analyser.minDecibels = -100;
|
||||
analyser.maxDecibels = 0;
|
||||
analyser.smoothingTimeConstant = 0.4;
|
||||
analyser.fftSize = 1024;
|
||||
mediaStreamSource.connect(analyser);
|
||||
|
||||
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
|
||||
const filteredAudio = (i: Uint8Array) => {
|
||||
const binFreq = audioContext.sampleRate / 2 / dataArray.length;
|
||||
return i.subarray(minFreq / binFreq, maxFreq / binFreq);
|
||||
};
|
||||
const peakVolume = (data: Uint8Array) => {
|
||||
const max = data.reduce((acc, v) => (v > acc ? v : acc), 0);
|
||||
return (maxVU - minVU) * (max / 256) + minVU;
|
||||
};
|
||||
|
||||
const canvas = ref.current!;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const t = setInterval(() => {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
const data = filteredAudio(dataArray);
|
||||
const vol = peakVolume(data);
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (full) {
|
||||
ctx.fillStyle = "#00FF00";
|
||||
for (let x = 0; x < data.length; x++) {
|
||||
const bx = data[x];
|
||||
const h = canvas.height / data.length;
|
||||
ctx.fillRect(0, x * h, (bx / 255) * canvas.width, h);
|
||||
}
|
||||
}
|
||||
|
||||
const barLen = ((vol - minVU) / (maxVU - minVU)) * canvas.height;
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(0, canvas.height - barLen, canvas.width, barLen);
|
||||
}, 50);
|
||||
|
||||
return () => {
|
||||
clearInterval(t);
|
||||
audioContext.close();
|
||||
};
|
||||
}
|
||||
}, [ref, track, full]);
|
||||
|
||||
return <canvas ref={ref} width={width ?? 200} height={height ?? 10} className={className}></canvas>;
|
||||
}
|
@ -1,25 +1,43 @@
|
||||
import { LiveKitRoom as LiveKitRoomContext, RoomAudioRenderer, useParticipants } from "@livekit/components-react";
|
||||
/* eslint-disable max-lines */
|
||||
import {
|
||||
LiveKitRoom as LiveKitRoomContext,
|
||||
RoomAudioRenderer,
|
||||
useEnsureRoom,
|
||||
useParticipantPermissions,
|
||||
useParticipants,
|
||||
} from "@livekit/components-react";
|
||||
import { dedupe, unixNow } from "@snort/shared";
|
||||
import { EventKind, NostrLink, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||
import { EventKind, EventPublisher, NostrLink, RequestBuilder, SystemInterface, TaggedNostrEvent } from "@snort/system";
|
||||
import { useRequestBuilder, useUserProfile } from "@snort/system-react";
|
||||
import { LocalParticipant, RemoteParticipant } from "livekit-client";
|
||||
import classNames from "classnames";
|
||||
import { LocalParticipant, LocalTrackPublication, RemoteParticipant, RoomEvent, Track } from "livekit-client";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import Text from "@/Components/Text/Text";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import { extractStreamInfo } from "@/Utils/stream";
|
||||
|
||||
import AsyncButton from "../Button/AsyncButton";
|
||||
import IconButton from "../Button/IconButton";
|
||||
import { ProxyImg } from "../ProxyImg";
|
||||
import Avatar from "../User/Avatar";
|
||||
import { AvatarGroup } from "../User/AvatarGroup";
|
||||
import DisplayName from "../User/DisplayName";
|
||||
import ProfileImage from "../User/ProfileImage";
|
||||
import VuBar from "./VU";
|
||||
|
||||
enum RoomTab {
|
||||
Participants,
|
||||
Chat,
|
||||
}
|
||||
|
||||
export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent; canJoin?: boolean }) {
|
||||
const { stream, service, id } = extractStreamInfo(ev);
|
||||
const { publisher } = useEventPublisher();
|
||||
const { publisher, system } = useEventPublisher();
|
||||
const [join, setJoin] = useState(false);
|
||||
const [token, setToken] = useState<string>();
|
||||
const [tab, setTab] = useState(RoomTab.Participants);
|
||||
|
||||
async function getToken() {
|
||||
if (!service || !publisher) return;
|
||||
@ -43,6 +61,17 @@ export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent; can
|
||||
}
|
||||
}
|
||||
|
||||
async function publishPresence(publisher: EventPublisher, system: SystemInterface) {
|
||||
const e = await publisher.generic(eb => {
|
||||
const aTag = NostrLink.fromEvent(ev).toEventTag();
|
||||
return eb
|
||||
.kind(10_312 as EventKind)
|
||||
.tag(aTag!)
|
||||
.tag(["expiration", (unixNow() + 60).toString()]);
|
||||
});
|
||||
await system.BroadcastEvent(e);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (join && !token) {
|
||||
getToken()
|
||||
@ -51,6 +80,18 @@ export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent; can
|
||||
}
|
||||
}, [join]);
|
||||
|
||||
useEffect(() => {
|
||||
if (token && publisher && system) {
|
||||
publishPresence(publisher, system);
|
||||
const t = setInterval(async () => {
|
||||
if (token) {
|
||||
publishPresence(publisher, system);
|
||||
}
|
||||
}, 60_000);
|
||||
return () => clearInterval(t);
|
||||
}
|
||||
}, [token, publisher, system]);
|
||||
|
||||
if (!join) {
|
||||
return (
|
||||
<div className="p flex flex-col gap-2">
|
||||
@ -65,8 +106,8 @@ export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent; can
|
||||
}
|
||||
return (
|
||||
<LiveKitRoomContext token={token} serverUrl={stream?.replace("wss+livekit://", "wss://")} connect={true}>
|
||||
<RoomAudioRenderer volume={1} />
|
||||
<ParticipantList ev={ev} />
|
||||
<RoomAudioRenderer volume={1} muted={false} />
|
||||
<RoomBody ev={ev} tab={tab} onSelectTab={setTab} />
|
||||
</LiveKitRoomContext>
|
||||
);
|
||||
}
|
||||
@ -76,9 +117,9 @@ function RoomHeader({ ev }: { ev: TaggedNostrEvent }) {
|
||||
return (
|
||||
<div className="relative rounded-xl h-[140px] w-full overflow-hidden">
|
||||
{image ? <ProxyImg src={image} className="w-full" /> : <div className="absolute bg-gray-dark w-full h-full" />}
|
||||
<div className="absolute left-4 top-4 w-full flex justify-between pr-4">
|
||||
<div className="absolute left-4 top-4 w-full flex justify-between pr-8">
|
||||
<div className="text-2xl">{title}</div>
|
||||
<div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<NostrParticipants ev={ev} />
|
||||
</div>
|
||||
</div>
|
||||
@ -86,19 +127,162 @@ function RoomHeader({ ev }: { ev: TaggedNostrEvent }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ParticipantList({ ev }: { ev: TaggedNostrEvent }) {
|
||||
const participants = useParticipants();
|
||||
function RoomBody({ ev, tab, onSelectTab }: { ev: TaggedNostrEvent; tab: RoomTab; onSelectTab: (t: RoomTab) => void }) {
|
||||
const participants = useParticipants({
|
||||
updateOnlyOn: [
|
||||
RoomEvent.ParticipantConnected,
|
||||
RoomEvent.ParticipantDisconnected,
|
||||
RoomEvent.ParticipantPermissionsChanged,
|
||||
RoomEvent.TrackMuted,
|
||||
RoomEvent.TrackPublished,
|
||||
RoomEvent.TrackUnmuted,
|
||||
RoomEvent.TrackUnmuted,
|
||||
],
|
||||
});
|
||||
return (
|
||||
<div className="p">
|
||||
<RoomHeader ev={ev} />
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Participants" />
|
||||
</h3>
|
||||
<div className="grid grid-cols-4">
|
||||
{participants.map(a => (
|
||||
<LiveKitUser p={a} key={a.identity} />
|
||||
))}
|
||||
<MyControls />
|
||||
<div className="flex text-center items-center text-xl font-medium mb-2">
|
||||
<div
|
||||
className={classNames("flex-1 py-2 cursor-pointer select-none border-b border-transparent", {
|
||||
"!border-highlight": tab === RoomTab.Participants,
|
||||
})}
|
||||
onClick={() => onSelectTab(RoomTab.Participants)}>
|
||||
<FormattedMessage defaultMessage="Participants" />
|
||||
</div>
|
||||
<div
|
||||
className={classNames("flex-1 py-2 cursor-pointer select-none border-b border-transparent", {
|
||||
"!border-highlight": tab === RoomTab.Chat,
|
||||
})}
|
||||
onClick={() => onSelectTab(RoomTab.Chat)}>
|
||||
<FormattedMessage defaultMessage="Chat" />
|
||||
</div>
|
||||
</div>
|
||||
{tab === RoomTab.Participants && (
|
||||
<div className="grid grid-cols-4">
|
||||
{participants.map(a => (
|
||||
<LiveKitUser p={a} key={a.identity} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{tab === RoomTab.Chat && (
|
||||
<>
|
||||
<RoomChat ev={ev} />
|
||||
<WriteChatMessage ev={ev} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MyControls() {
|
||||
const room = useEnsureRoom();
|
||||
const p = room.localParticipant;
|
||||
const permissions = useParticipantPermissions({
|
||||
participant: p,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (permissions && p instanceof LocalParticipant) {
|
||||
const handler = (lt: LocalTrackPublication) => {
|
||||
lt.mute();
|
||||
};
|
||||
p.on("localTrackPublished", handler);
|
||||
if (permissions.canPublish && p.audioTrackPublications.size === 0) {
|
||||
p.setMicrophoneEnabled(true);
|
||||
}
|
||||
return () => {
|
||||
p.off("localTrackPublished", handler);
|
||||
};
|
||||
}
|
||||
}, [p, permissions]);
|
||||
const isMuted = p.getTrackPublication(Track.Source.Microphone)?.isMuted ?? true;
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center mt-2">
|
||||
{p.permissions?.canPublish && (
|
||||
<IconButton
|
||||
icon={{ name: !isMuted ? "mic" : "mic-off", size: 20 }}
|
||||
onClick={async () => {
|
||||
if (isMuted) {
|
||||
await p.setMicrophoneEnabled(true);
|
||||
} else {
|
||||
await p.setMicrophoneEnabled(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/*<IconButton icon={{ name: "hand", size: 20 }} />*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RoomChat({ ev }: { ev: TaggedNostrEvent }) {
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
const sub = useMemo(() => {
|
||||
const sub = new RequestBuilder(`room-chat:${link.tagKey}`);
|
||||
sub.withOptions({ leaveOpen: true, replaceable: true });
|
||||
sub.withFilter().replyToLink([link]).kinds([EventKind.LiveEventChat]).limit(100);
|
||||
return sub;
|
||||
}, [link.tagKey]);
|
||||
const chat = useRequestBuilder(sub);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100dvh-370px)] overflow-x-hidden overflow-y-scroll">
|
||||
<div className="flex flex-col gap-1 flex-col-reverse w-full">
|
||||
{chat
|
||||
.sort((a, b) => b.created_at - a.created_at)
|
||||
.map(e => (
|
||||
<ChatMessage key={e.id} ev={e} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatMessage({ ev }: { ev: TaggedNostrEvent }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[auto_1fr] items-center gap-2">
|
||||
<ProfileImage
|
||||
pubkey={ev.pubkey}
|
||||
size={20}
|
||||
showBadges={false}
|
||||
showFollowDistance={false}
|
||||
className="text-highlight"
|
||||
/>
|
||||
<Text id={ev.id} content={ev.content} creator={ev.pubkey} tags={ev.tags} disableMedia={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WriteChatMessage({ ev }: { ev: TaggedNostrEvent }) {
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
const [chat, setChat] = useState("");
|
||||
const { publisher, system } = useEventPublisher();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
async function sendMessage() {
|
||||
if (!publisher || !system || chat.length < 2) return;
|
||||
const eChat = await publisher.generic(eb => eb.kind(EventKind.LiveEventChat).tag(link.toEventTag()!).content(chat));
|
||||
await system.BroadcastEvent(eChat);
|
||||
setChat("");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<input
|
||||
type="text"
|
||||
value={chat}
|
||||
placeholder={formatMessage({ defaultMessage: "Write message" })}
|
||||
onChange={e => setChat(e.target.value)}
|
||||
className="grow"
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter") {
|
||||
sendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton icon={{ name: "arrow-right" }} onClick={sendMessage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -116,16 +300,27 @@ function NostrParticipants({ ev }: { ev: TaggedNostrEvent }) {
|
||||
}, [link.tagKey]);
|
||||
|
||||
const presense = useRequestBuilder(sub);
|
||||
return <AvatarGroup ids={dedupe(presense.map(a => a.pubkey))} size={32} />;
|
||||
const filteredPresence = presense.filter(ev => ev.created_at > unixNow() - 600);
|
||||
return <AvatarGroup ids={dedupe(filteredPresence.map(a => a.pubkey))} size={32} />;
|
||||
}
|
||||
|
||||
function LiveKitUser({ p }: { p: RemoteParticipant | LocalParticipant }) {
|
||||
const pubkey = p.identity.startsWith("guest-") ? "anon" : p.identity;
|
||||
const profile = useUserProfile(pubkey);
|
||||
const mic = p.getTrackPublication(Track.Source.Microphone);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<Avatar pubkey={pubkey} className={p.isSpeaking ? "outline" : ""} user={profile} size={48} />
|
||||
<DisplayName pubkey={pubkey} user={pubkey === "anon" ? { name: "Anon" } : profile} />
|
||||
<div className="relative w-[45px] h-[45px] flex items-center justify-center rounded-full overflow-hidden">
|
||||
{mic?.audioTrack?.mediaStreamTrack && (
|
||||
<VuBar track={mic.audioTrack?.mediaStreamTrack} className="absolute h-full w-full" />
|
||||
)}
|
||||
<Avatar pubkey={pubkey} user={profile} size={40} className="absolute" />
|
||||
</div>
|
||||
<div>
|
||||
<DisplayName pubkey={pubkey} user={pubkey === "anon" ? { name: "Anon" } : profile} />
|
||||
{p.permissions?.canPublish && <div className="text-highlight">Speaker</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ export default function ProfileImage({
|
||||
const classNamesOverInner = classNames(
|
||||
"min-w-0",
|
||||
{
|
||||
"grid grid-cols-[min-content_auto] gap-3 items-center": showUsername,
|
||||
"grid grid-cols-[min-content_auto] gap-2 items-center": showUsername,
|
||||
},
|
||||
className,
|
||||
);
|
||||
|
@ -1431,6 +1431,9 @@
|
||||
"W9355R": {
|
||||
"defaultMessage": "Unmute"
|
||||
},
|
||||
"WTrOy3": {
|
||||
"defaultMessage": "Chat"
|
||||
},
|
||||
"WeLEuL": {
|
||||
"defaultMessage": "From Server"
|
||||
},
|
||||
@ -1485,6 +1488,9 @@
|
||||
"YH2RKk": {
|
||||
"defaultMessage": "Popular media servers."
|
||||
},
|
||||
"YLGfQn": {
|
||||
"defaultMessage": "Write message"
|
||||
},
|
||||
"YQZY/S": {
|
||||
"defaultMessage": "It looks like you dont follow enough people, take a look at {newUsersPage} to discover people to follow!"
|
||||
},
|
||||
|
@ -474,6 +474,7 @@
|
||||
"W2PiAr": "{n} Blocked",
|
||||
"W4SaxY": "Local",
|
||||
"W9355R": "Unmute",
|
||||
"WTrOy3": "Chat",
|
||||
"WeLEuL": "From Server",
|
||||
"Wj5TbN": "Issues",
|
||||
"WmZhfL": "Automatically translate notes to your local language",
|
||||
@ -492,6 +493,7 @@
|
||||
"YDMrKK": "Users",
|
||||
"YDURw6": "Service URL",
|
||||
"YH2RKk": "Popular media servers.",
|
||||
"YLGfQn": "Write message",
|
||||
"YQZY/S": "It looks like you dont follow enough people, take a look at {newUsersPage} to discover people to follow!",
|
||||
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
|
||||
"YU7ZYp": "Public Chat",
|
||||
|
@ -54,7 +54,8 @@ const enum EventKind {
|
||||
|
||||
LongFormTextNote = 30023, // NIP-23
|
||||
AppData = 30_078, // NIP-78
|
||||
LiveEvent = 30311, // NIP-102
|
||||
LiveEvent = 30311, // NIP-53
|
||||
LiveEventChat = 1311, // NIP-53
|
||||
UserStatus = 30315, // NIP-38
|
||||
ZapstrTrack = 31337,
|
||||
ApplicationHandler = 31_990,
|
||||
|
Reference in New Issue
Block a user