From b602a5836c45b664ecce82291ba8d31729b1ae50 Mon Sep 17 00:00:00 2001 From: nocnico Date: Fri, 25 Jul 2025 17:19:58 +0200 Subject: [PATCH 01/11] Ghostmode fix now? --- apps/dispatch-server/routes/dispatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dispatch-server/routes/dispatcher.ts b/apps/dispatch-server/routes/dispatcher.ts index 7adc9c6d..03cc314b 100644 --- a/apps/dispatch-server/routes/dispatcher.ts +++ b/apps/dispatch-server/routes/dispatcher.ts @@ -34,7 +34,7 @@ router.patch("/:id", async (req, res) => { }, }); - if (discordAccount?.id) { + if (discordAccount?.id && !disaptcherUpdate.ghostMode) { await renameMember( discordAccount.discordId.toString(), `${getPublicUser(newDispatcher.user).fullName} • ${newDispatcher.zone}`, From 2671571bfbb0032fce069aea1e9cdc68537adee5 Mon Sep 17 00:00:00 2001 From: nocnico Date: Fri, 25 Jul 2025 17:43:05 +0200 Subject: [PATCH 02/11] maybe ghostmode fix now? --- apps/dispatch-server/socket-events/connect-dispatch.ts | 2 +- apps/dispatch/app/_store/audioStore.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/dispatch-server/socket-events/connect-dispatch.ts b/apps/dispatch-server/socket-events/connect-dispatch.ts index 853e1d7c..f2acb6c9 100644 --- a/apps/dispatch-server/socket-events/connect-dispatch.ts +++ b/apps/dispatch-server/socket-events/connect-dispatch.ts @@ -99,7 +99,7 @@ export const handleConnectDispatch = io.to("dispatchers").emit("dispatchers-update"); io.to("pilots").emit("dispatchers-update"); - if (discordAccount?.id && !ghostMode) { + if (discordAccount?.id) { await renameMember( discordAccount.discordId.toString(), `${getPublicUser(user).fullName} - ${user.publicId}`, diff --git a/apps/dispatch/app/_store/audioStore.ts b/apps/dispatch/app/_store/audioStore.ts index 0fb63b4d..b1b789c0 100644 --- a/apps/dispatch/app/_store/audioStore.ts +++ b/apps/dispatch/app/_store/audioStore.ts @@ -177,6 +177,7 @@ export const useAudioStore = create((set, get) => ({ if (dispatchState.status === "connected" && dispatchState.connectedDispatcher?.id) { changeDispatcherAPI(dispatchState.connectedDispatcher?.id, { zone: roomName, + ghostMode: dispatchState.ghostMode, }); } From 54e0bc0b12c94a023a53aa16ed5451f2ff1ec416 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:53:58 -0700 Subject: [PATCH 03/11] fixed #104 --- apps/hub-server/modules/event.ts | 92 +++++++++++++++++--------------- apps/hub-server/modules/mail.ts | 62 +++++++++++++-------- 2 files changed, 88 insertions(+), 66 deletions(-) diff --git a/apps/hub-server/modules/event.ts b/apps/hub-server/modules/event.ts index c5bead02..363c0b1b 100644 --- a/apps/hub-server/modules/event.ts +++ b/apps/hub-server/modules/event.ts @@ -7,55 +7,59 @@ export const handleParticipantFinished = async ( participant: Participant, user: User, ) => { - const discordAccount = await prisma.discordAccount.findFirst({ - where: { - userId: user.id, - }, - }); - - const badgedToAdd = event.finishedBadges.filter((badge) => { - return !user.badges.includes(badge); - }); - const permissionsToAdd = event.finishedPermissions.filter((permission) => { - return !user.permissions.includes(permission); - }); - - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - badges: { - push: badgedToAdd, + try { + const discordAccount = await prisma.discordAccount.findFirst({ + where: { + userId: user.id, }, - permissions: { - push: permissionsToAdd, - }, - }, - }); - - if (discordAccount) { - await setStandardName({ - memberId: discordAccount.discordId, - userId: user.id, }); - } - await sendCourseCompletedEmail(user.email, user, event); - await prisma.participant.update({ - where: { - id: participant.id, - }, - data: { - statusLog: { - push: { - event: "Berechtigungen und Badges vergeben", - timestamp: new Date(), - user: "system", + const badgedToAdd = event.finishedBadges.filter((badge) => { + return !user.badges.includes(badge); + }); + const permissionsToAdd = event.finishedPermissions.filter((permission) => { + return !user.permissions.includes(permission); + }); + + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + badges: { + push: badgedToAdd, + }, + permissions: { + push: permissionsToAdd, }, }, - }, - }); + }); + + if (discordAccount) { + await setStandardName({ + memberId: discordAccount.discordId, + userId: user.id, + }); + } + await sendCourseCompletedEmail(user.email, user, event); + + await prisma.participant.update({ + where: { + id: participant.id, + }, + data: { + statusLog: { + push: { + event: "Berechtigungen und Badges vergeben", + timestamp: new Date(), + user: "system", + }, + }, + }, + }); + } catch (error) { + console.error("Error handling participant finished:", error); + } }; export const handleParticipantEnrolled = async ( diff --git a/apps/hub-server/modules/mail.ts b/apps/hub-server/modules/mail.ts index 29bd2907..d8d0beeb 100644 --- a/apps/hub-server/modules/mail.ts +++ b/apps/hub-server/modules/mail.ts @@ -34,43 +34,61 @@ const initTransporter = () => { initTransporter(); export const sendCourseCompletedEmail = async (to: string, user: User, event: Event) => { - const emailHtml = await renderCourseCompleted({ user, event }); + try { + const emailHtml = await renderCourseCompleted({ user, event }); - if (!transporter) { - console.error("Transporter is not initialized"); - return; + if (!transporter) { + console.error("Transporter is not initialized"); + return; + } + await sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml); + } catch (error) { + console.error("Error sending course completed email:", error); } - sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml); }; export const sendPasswordChanged = async (to: string, user: User, password: string) => { - const emailHtml = await renderPasswordChanged({ user, password }); + try { + const emailHtml = await renderPasswordChanged({ user, password }); - await sendMail(to, `Dein Passwort wurde geändert`, emailHtml); + await sendMail(to, `Dein Passwort wurde geändert`, emailHtml); + } catch (error) {} }; export const sendEmailVerification = async (to: string, user: User, code: string) => { - const emailHtml = await renderVerificationCode({ - user, - code, - }); - await sendMail(to, "Bestätige deine E-Mail-Adresse", emailHtml); + try { + const emailHtml = await renderVerificationCode({ + user, + code, + }); + await sendMail(to, "Bestätige deine E-Mail-Adresse", emailHtml); + } catch (error) { + console.error("Error sending email verification:", error); + } }; export const sendBannEmail = async (to: string, user: User, staffName: string) => { - const emailHtml = await renderBannNotice({ - user, - staffName, - }); - await sendMail(to, "Deine Sperrung bei Virtual Air Rescue", emailHtml); + try { + const emailHtml = await renderBannNotice({ + user, + staffName, + }); + await sendMail(to, "Deine Sperrung bei Virtual Air Rescue", emailHtml); + } catch (error) { + console.error("Error sending ban email:", error); + } }; export const sendTimebannEmail = async (to: string, user: User, staffName: string) => { - const emailHtml = await renderTimeBanNotice({ - user, - staffName, - }); - await sendMail(to, "Deine vorrübergehende Sperrung bei Virtual Air Rescue", emailHtml); + try { + const emailHtml = await renderTimeBanNotice({ + user, + staffName, + }); + await sendMail(to, "Deine vorrübergehende Sperrung bei Virtual Air Rescue", emailHtml); + } catch (error) { + console.error("Error sending time ban email:", error); + } }; export const sendMail = async (to: string, subject: string, html: string) => From 1b23aede89e05a5549c2326996e9a0f7d6e4adfb Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:04:44 -0700 Subject: [PATCH 04/11] fixed #105 --- apps/dispatch-server/modules/mission.ts | 195 ++++++++++++------------ apps/dispatch-server/modules/ntfy.ts | 19 +-- apps/dispatch-server/routes/mission.ts | 6 +- 3 files changed, 115 insertions(+), 105 deletions(-) diff --git a/apps/dispatch-server/modules/mission.ts b/apps/dispatch-server/modules/mission.ts index e79c95a8..4f9fe07c 100644 --- a/apps/dispatch-server/modules/mission.ts +++ b/apps/dispatch-server/modules/mission.ts @@ -14,113 +14,118 @@ export const sendAlert = async ( connectedAircrafts: ConnectedAircraft[]; mission: Mission; }> => { - const mission = await prisma.mission.findUnique({ - where: { id: id }, - }); - const Stations = await prisma.station.findMany({ - where: { - id: { - in: mission?.missionStationIds, - }, - }, - }); - - if (!mission) { - throw new Error("Mission not found"); - } - - // connectedAircrafts the alert is sent to - const connectedAircrafts = await prisma.connectedAircraft.findMany({ - where: { - stationId: stationId - ? stationId - : { - in: mission.missionStationIds, - }, - logoutTime: null, - }, - include: { - Station: true, - }, - }); - - for (const aircraft of connectedAircrafts) { - io.to(`station:${aircraft.stationId}`).emit("mission-alert", { - ...mission, - Stations, + try { + const mission = await prisma.mission.findUnique({ + where: { id: id }, }); - io.to(`desktop:${aircraft.userId}`).emit("mission-alert", { - missionId: mission.id, - }); - - const user = await prisma.user.findUnique({ - where: { id: aircraft.userId }, - }); - if (!user) continue; - if (user.settingsNtfyRoom) { - await sendNtfyMission(mission, Stations, aircraft.Station, user.settingsNtfyRoom); - } - const existingMissionOnStationUser = await prisma.missionOnStationUsers.findFirst({ + const Stations = await prisma.station.findMany({ where: { - missionId: mission.id, - userId: aircraft.userId, - stationId: aircraft.stationId, + id: { + in: mission?.missionStationIds, + }, }, }); - if (!existingMissionOnStationUser) - await prisma.missionOnStationUsers.create({ - data: { + if (!mission) { + throw new Error("Mission not found"); + } + + // connectedAircrafts the alert is sent to + const connectedAircrafts = await prisma.connectedAircraft.findMany({ + where: { + stationId: stationId + ? stationId + : { + in: mission.missionStationIds, + }, + logoutTime: null, + }, + include: { + Station: true, + }, + }); + + for (const aircraft of connectedAircrafts) { + io.to(`station:${aircraft.stationId}`).emit("mission-alert", { + ...mission, + Stations, + }); + io.to(`desktop:${aircraft.userId}`).emit("mission-alert", { + missionId: mission.id, + }); + + const user = await prisma.user.findUnique({ + where: { id: aircraft.userId }, + }); + if (!user) continue; + if (user.settingsNtfyRoom) { + await sendNtfyMission(mission, Stations, aircraft.Station, user.settingsNtfyRoom); + } + const existingMissionOnStationUser = await prisma.missionOnStationUsers.findFirst({ + where: { missionId: mission.id, userId: aircraft.userId, stationId: aircraft.stationId, }, }); - } - // for statistics only - await prisma.missionsOnStations - .createMany({ - data: mission.missionStationIds.map((stationId) => ({ - missionId: mission.id, - stationId, - })), - }) - .catch((err) => { - // Ignore if the entry already exists - }); - if (user === "HPG") { - await prisma.mission.update({ - where: { id: Number(id) }, - data: { - state: "running", - missionLog: { - push: { - type: "alert-log", - auto: true, - timeStamp: new Date().toISOString(), - } as any, + if (!existingMissionOnStationUser) + await prisma.missionOnStationUsers.create({ + data: { + missionId: mission.id, + userId: aircraft.userId, + stationId: aircraft.stationId, + }, + }); + } + + // for statistics only + await prisma.missionsOnStations + .createMany({ + data: mission.missionStationIds.map((stationId) => ({ + missionId: mission.id, + stationId, + })), + }) + .catch((err) => { + // Ignore if the entry already exists + }); + if (user === "HPG") { + await prisma.mission.update({ + where: { id: Number(id) }, + data: { + state: "running", + missionLog: { + push: { + type: "alert-log", + auto: true, + timeStamp: new Date().toISOString(), + } as any, + }, }, - }, - }); - } else { - await prisma.mission.update({ - where: { id: Number(id) }, - data: { - state: "running", - missionLog: { - push: { - type: "alert-log", - auto: false, - timeStamp: new Date().toISOString(), - data: { - stationId: stationId, - user: getPublicUser(user, { ignorePrivacy: true }), - }, - } as any, + }); + } else { + await prisma.mission.update({ + where: { id: Number(id) }, + data: { + state: "running", + missionLog: { + push: { + type: "alert-log", + auto: false, + timeStamp: new Date().toISOString(), + data: { + stationId: stationId, + user: getPublicUser(user, { ignorePrivacy: true }), + }, + } as any, + }, }, - }, - }); + }); + } + return { connectedAircrafts, mission }; + } catch (error) { + console.error("Error sending mission alert:", error); + throw new Error("Ein Fehler ist aufgetreten. Bitte melde den Fehler als Bug"); } - return { connectedAircrafts, mission }; }; diff --git a/apps/dispatch-server/modules/ntfy.ts b/apps/dispatch-server/modules/ntfy.ts index bc86df56..3ba75dde 100644 --- a/apps/dispatch-server/modules/ntfy.ts +++ b/apps/dispatch-server/modules/ntfy.ts @@ -50,10 +50,7 @@ const getRthCallsigns = (mission: Mission, stations: Station[]) => { return `🚁 RTH${callsigns.length > 1 ? "s" : ""}: ${callsigns.join(" / ")} `; }; -const getNtfyHeader = ( - mission: Mission, - clientStation: Station, -): NtfyHeader => ({ +const getNtfyHeader = (mission: Mission, clientStation: Station): NtfyHeader => ({ headers: { Title: `${clientStation.bosCallsignShort} / ${mission.missionKeywordAbbreviation} / ${mission.missionKeywordCategory}`, Tags: "pager", @@ -76,9 +73,13 @@ export const sendNtfyMission = async ( clientStation: Station, ntfyRoom: string, ) => { - axios.post( - `https://ntfy.sh/${ntfyRoom}`, - getNtfyData(mission, stations), - getNtfyHeader(mission, clientStation), - ); + try { + await axios.post( + `https://ntfy.sh/${ntfyRoom}`, + getNtfyData(mission, stations), + getNtfyHeader(mission, clientStation), + ); + } catch (error) { + console.error("Error sending Ntfy mission:", error); + } }; diff --git a/apps/dispatch-server/routes/mission.ts b/apps/dispatch-server/routes/mission.ts index bcb18a2a..e487e59f 100644 --- a/apps/dispatch-server/routes/mission.ts +++ b/apps/dispatch-server/routes/mission.ts @@ -189,7 +189,11 @@ router.post("/:id/send-alert", async (req, res) => { return; } catch (error) { console.error(error); - res.status(500).json({ error: "Failed to send mission" }); + res + .status(500) + .json({ + error: `Ein Fehler ist aufgetreten. Bitte melde den Fehler als Bug (${(error as Error).message})`, + }); return; } }); From afe32306d3f5dedbb1b46d3a348ad0947fef9110 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:32:35 -0700 Subject: [PATCH 05/11] fixed #106 --- packages/database/prisma/json/User.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/database/prisma/json/User.ts b/packages/database/prisma/json/User.ts index 3c91a500..1756f152 100644 --- a/packages/database/prisma/json/User.ts +++ b/packages/database/prisma/json/User.ts @@ -22,13 +22,16 @@ export const getPublicUser = ( }, ): PublicUser => { const lastName = user.lastname + .trim() .split(" ") - .map((part) => `${part[0]}.`) + .map((part) => `${part[0] || ""}.`) .join(" "); + return { firstname: user.firstname, - lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName, // Only take the first letter of each section of the last name - fullName: `${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName}`, + lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName.trim(), // Only take the first letter of each section of the last name + fullName: + `${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName}`.trim(), publicId: user.publicId, badges: user.badges, }; From 14ea5fcf5564b5b7e65e8f55e670b8039df7291b Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:49:50 -0700 Subject: [PATCH 06/11] fixed User argument is missing on Discord-connect --- apps/hub/app/api/discord-redirect/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/hub/app/api/discord-redirect/route.ts b/apps/hub/app/api/discord-redirect/route.ts index fdd52ad5..daff0752 100644 --- a/apps/hub/app/api/discord-redirect/route.ts +++ b/apps/hub/app/api/discord-redirect/route.ts @@ -58,7 +58,7 @@ export const GET = async (req: NextRequest) => { email: discordUser.email, avatar: discordUser.avatar, username: discordUser.username, - globalName: discordUser.global_name, + globalName: discordUser.global_name || discordUser.username, verified: discordUser.verified, tokenType: authData.token_type, } as DiscordAccount; From f534bbc902048f65ce383c2b031b9b0cd1e5fbe4 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:53:22 -0700 Subject: [PATCH 07/11] implemented #112 --- .../admin/changelog/_components/Form.tsx | 19 +++++++++++++++++++ apps/hub/app/(app)/admin/changelog/action.ts | 12 +++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/hub/app/(app)/admin/changelog/_components/Form.tsx b/apps/hub/app/(app)/admin/changelog/_components/Form.tsx index de9abaf2..0700d499 100644 --- a/apps/hub/app/(app)/admin/changelog/_components/Form.tsx +++ b/apps/hub/app/(app)/admin/changelog/_components/Form.tsx @@ -11,6 +11,7 @@ import { Button } from "../../../../_components/ui/Button"; import { redirect } from "next/navigation"; import dynamic from "next/dynamic"; import toast from "react-hot-toast"; +import { cn } from "@repo/shared-components"; const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), { ssr: false }); @@ -24,6 +25,7 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => { previewImage: changelog?.previewImage || "", // Changed to accept a URL as a string }, }); + const [skipUserUpdate, setSkipUserUpdate] = useState(false); const [markdownText, setMarkdownText] = useState(changelog?.text || ""); const [imageError, setImageError] = useState(false); const [showImage, setShowImage] = useState(false); @@ -61,6 +63,9 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => { text: markdownText, }, changelog?.id, + { + skipUserUpdate: skipUserUpdate, + }, ); toast.success("Daten gespeichert"); if (!changelog) redirect(`/admin/changelog`); @@ -96,6 +101,7 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => { {(() => { if (showImage && isValidImageUrl(previewImage) && !imageError) { return ( + // eslint-disable-next-line @next/next/no-img-element Preview {
+ {!changelog?.id && ( + + )}
From 1da23c3412641e3d87ebda1d976df84cbe72fadd Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 25 Jul 2025 21:02:18 -0700 Subject: [PATCH 09/11] resolves #116 --- apps/dispatch/Dockerfile | 2 ++ docker-compose.prod.yml | 1 + docker-compose.staging.yml | 1 + 3 files changed, 4 insertions(+) diff --git a/apps/dispatch/Dockerfile b/apps/dispatch/Dockerfile index 0923d88c..315d8d76 100644 --- a/apps/dispatch/Dockerfile +++ b/apps/dispatch/Dockerfile @@ -6,12 +6,14 @@ ARG NEXT_PUBLIC_HUB_URL ARG NEXT_PUBLIC_DISPATCH_SERVICE_ID ARG NEXT_PUBLIC_LIVEKIT_URL ARG NEXT_PUBLIC_DISCORD_URL +ARG NEXT_PUBLIC_OPENAIP_ACCESS ENV NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL ENV NEXT_PUBLIC_DISPATCH_URL=$NEXT_PUBLIC_DISPATCH_URL ENV NEXT_PUBLIC_HUB_URL=$NEXT_PUBLIC_HUB_URL ENV NEXT_PUBLIC_DISPATCH_SERVICE_ID=$NEXT_PUBLIC_DISPATCH_SERVICE_ID ENV NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL +ENV NEXT_PUBLIC_OPENAIP_ACCESS=$NEXT_PUBLIC_OPENAIP_ACCESS ENV NEXT_PUBLIC_DISCORD_URL=$NEXT_PUBLIC_DISCORD_URL ENV PNPM_HOME="/usr/local/pnpm" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 874b5331..9808f0e2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -79,6 +79,7 @@ services: - NEXT_PUBLIC_DISPATCH_SERVICE_ID=1 - NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL - NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL + - NEXT_PUBLIC_OPENAIP_ACCESS=$NEXT_PUBLIC_OPENAIP_ACCESS env_file: - .env.prod deploy: diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 168abd4d..78fc2945 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -74,6 +74,7 @@ services: - NEXT_PUBLIC_DISPATCH_SERVICE_ID=1 - NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL - NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL + - NEXT_PUBLIC_OPENAIP_ACCESS=$NEXT_PUBLIC_OPENAIP_ACCESS env_file: - .env.prod deploy: From 2abdbf168fc76d59fd645d71f9f70a64ed3ee223 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 25 Jul 2025 21:30:03 -0700 Subject: [PATCH 10/11] fixes #117 --- apps/dispatch/app/_components/Audio/Audio.tsx | 36 +++++----- apps/dispatch/app/_store/audioStore.ts | 66 ++++++++++++------- 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/apps/dispatch/app/_components/Audio/Audio.tsx b/apps/dispatch/app/_components/Audio/Audio.tsx index abe069a6..8b908570 100644 --- a/apps/dispatch/app/_components/Audio/Audio.tsx +++ b/apps/dispatch/app/_components/Audio/Audio.tsx @@ -25,6 +25,7 @@ import { useSounds } from "_components/Audio/useSounds"; export const Audio = () => { const { speakingParticipants, + resetSpeakingParticipants, isTalking, toggleTalking, transmitBlocked, @@ -104,7 +105,7 @@ export const Audio = () => { data-tip="Nachricht entfernen" > {state === "connected" && (
- {connectionQuality === ConnectionQuality.Excellent && } - {connectionQuality === ConnectionQuality.Good && } - {connectionQuality === ConnectionQuality.Poor && } - {connectionQuality === ConnectionQuality.Lost && } + {connectionQuality === ConnectionQuality.Excellent && } + {connectionQuality === ConnectionQuality.Good && } + {connectionQuality === ConnectionQuality.Poor && } + {connectionQuality === ConnectionQuality.Lost && } {connectionQuality === ConnectionQuality.Unknown && ( - + )}
{remoteParticipants}
@@ -184,7 +186,7 @@ export const Audio = () => { {ROOMS.map((r) => (
  • @@ -201,12 +203,12 @@ export const Audio = () => { ))}
  • diff --git a/apps/dispatch/app/_store/audioStore.ts b/apps/dispatch/app/_store/audioStore.ts index b1b789c0..c0aaefe1 100644 --- a/apps/dispatch/app/_store/audioStore.ts +++ b/apps/dispatch/app/_store/audioStore.ts @@ -25,28 +25,29 @@ import { usePilotConnectionStore } from "_store/pilot/connectionStore"; let interval: NodeJS.Timeout; type TalkState = { + addSpeakingParticipant: (participant: Participant) => void; + connect: (roomName: string, role: string) => void; + connectionQuality: ConnectionQuality; + disconnect: () => void; + isTalking: boolean; + localRadioTrack: LocalTrackPublication | undefined; + message: string | null; + removeMessage: () => void; + removeSpeakingParticipant: (speakingParticipants: Participant) => void; + remoteParticipants: number; + resetSpeakingParticipants: (source: string) => void; + room: Room | null; + setSettings: (settings: Partial) => void; settings: { micDeviceId: string | null; micVolume: number; radioVolume: number; dmeVolume: number; }; - isTalking: boolean; - transmitBlocked: boolean; - removeMessage: () => void; - state: "connecting" | "connected" | "disconnected" | "error"; - message: string | null; - connectionQuality: ConnectionQuality; - remoteParticipants: number; - toggleTalking: () => void; - setSettings: (settings: Partial) => void; - connect: (roomName: string, role: string) => void; - disconnect: () => void; speakingParticipants: Participant[]; - addSpeakingParticipant: (participant: Participant) => void; - removeSpeakingParticipant: (speakingParticipants: Participant) => void; - room: Room | null; - localRadioTrack: LocalTrackPublication | undefined; + state: "connecting" | "connected" | "disconnected" | "error"; + toggleTalking: () => void; + transmitBlocked: boolean; }; const getToken = async (roomName: string) => { const response = await axios.get(`/api/livekit-token?roomName=${roomName}`); @@ -71,6 +72,15 @@ export const useAudioStore = create((set, get) => ({ remoteParticipants: 0, connectionQuality: ConnectionQuality.Unknown, room: null, + resetSpeakingParticipants: (source: string) => { + set({ + speakingParticipants: [], + isTalking: false, + transmitBlocked: false, + message: `Ruf beendet durch ${source || "eine unsichtbare Macht"}`, + }); + get().room?.localParticipant.setMicrophoneEnabled(false); + }, addSpeakingParticipant: (participant) => { set((state) => { if (!state.speakingParticipants.some((p) => p.identity === participant.identity)) { @@ -201,10 +211,15 @@ export const useAudioStore = create((set, get) => ({ set({ localRadioTrack: publishedTrack }); - set({ state: "connected", room, message: null }); + set({ state: "connected", room, isTalking: false, message: null }); }) .on(RoomEvent.Disconnected, () => { - set({ state: "disconnected", speakingParticipants: [], transmitBlocked: false }); + set({ + state: "disconnected", + speakingParticipants: [], + transmitBlocked: false, + isTalking: false, + }); handleDisconnect(); }) @@ -223,17 +238,22 @@ export const useAudioStore = create((set, get) => ({ room.registerRpcMethod("force-mute", async (data: RpcInvocationData) => { const { by } = JSON.parse(data.payload); - room.localParticipant.setMicrophoneEnabled(false); - useAudioStore.setState({ - isTalking: false, - message: `Ruf beendet durch ${by || "eine unsichtbare Macht"}`, - }); - return `Hello, ${data.callerIdentity}!`; + get().resetSpeakingParticipants(by); + return "OK"; }); interval = setInterval(() => { + // Filter forgotten participants + const oldSpeakingParticipants = get().speakingParticipants; + const speakingParticipants = oldSpeakingParticipants.filter((oP) => { + return Array.from(room.remoteParticipants.values()).find( + (p) => p.identity === oP.identity, + ); + }); + set({ remoteParticipants: room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed + speakingParticipants, }); }, 500); } catch (error: Error | unknown) { From 5b57b31706123de3fccb079a11f6b5783df01b90 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 25 Jul 2025 22:37:46 -0700 Subject: [PATCH 11/11] fixes #118 --- apps/dispatch/app/_components/left/Chat.tsx | 32 ++++++++++++++++++--- apps/dispatch/app/_store/leftMenuStore.ts | 10 +++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/apps/dispatch/app/_components/left/Chat.tsx b/apps/dispatch/app/_components/left/Chat.tsx index d09a5ebf..93b17997 100644 --- a/apps/dispatch/app/_components/left/Chat.tsx +++ b/apps/dispatch/app/_components/left/Chat.tsx @@ -10,9 +10,11 @@ import { getConnectedDispatcherAPI } from "_querys/dispatcher"; import { getConnectedAircraftsAPI } from "_querys/aircrafts"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { usePilotConnectionStore } from "_store/pilot/connectionStore"; +import { Trash } from "lucide-react"; export const Chat = () => { const { + removeChat, setReportTabOpen, chatOpen, setChatOpen, @@ -50,13 +52,21 @@ export const Chat = () => { setOwnId(session.data?.user.id); }, [session.data?.user.id, setOwnId]); - const filteredDispatcher = dispatcher?.filter((d) => d.userId !== session.data?.user.id); + const filteredDispatcher = dispatcher?.filter( + (d) => d.userId !== session.data?.user.id && !chats[d.userId], + ); const filteredAircrafts = aircrafts?.filter( - (a) => a.userId !== session.data?.user.id && dispatcherConnected, + (a) => a.userId !== session.data?.user.id && dispatcherConnected && chats[a.userId], ); const btnActive = pilotConnected || dispatcherConnected; + useEffect(() => { + if (!filteredDispatcher?.length && !filteredAircrafts?.length) { + setAddTabValue("default"); + } + }, [filteredDispatcher, filteredAircrafts]); + useEffect(() => { if (!btnActive) { setChatOpen(false); @@ -146,13 +156,17 @@ export const Chat = () => {
    {/* So macht man kein overflow handeling, weiß ich. Aber es funktioniert... */} {chat.messages.map((chatMessage) => { @@ -208,6 +222,16 @@ export const Chat = () => { )} {selectedChat && (
    +