update FDISK sync
This commit is contained in:
@@ -17,6 +17,16 @@ const ChatMessageView: React.FC = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
|
const prevActiveRef = useRef(false);
|
||||||
|
|
||||||
|
// Invalidate messages immediately when this room + panel becomes active
|
||||||
|
useEffect(() => {
|
||||||
|
const active = !!selectedRoomToken && chatPanelOpen;
|
||||||
|
if (active && !prevActiveRef.current) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'messages', selectedRoomToken] });
|
||||||
|
}
|
||||||
|
prevActiveRef.current = active;
|
||||||
|
}, [selectedRoomToken, chatPanelOpen, queryClient]);
|
||||||
|
|
||||||
const room = rooms.find((r) => r.token === selectedRoomToken);
|
const room = rooms.find((r) => r.token === selectedRoomToken);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,27 @@ import type { Notification, NotificationSchwere } from '../../types/notification
|
|||||||
|
|
||||||
const POLL_INTERVAL_MS = 15_000; // 15 seconds
|
const POLL_INTERVAL_MS = 15_000; // 15 seconds
|
||||||
|
|
||||||
|
function playNotificationSound() {
|
||||||
|
try {
|
||||||
|
const ctx = new AudioContext();
|
||||||
|
const oscillator = ctx.createOscillator();
|
||||||
|
const gain = ctx.createGain();
|
||||||
|
oscillator.connect(gain);
|
||||||
|
gain.connect(ctx.destination);
|
||||||
|
oscillator.type = 'sine';
|
||||||
|
oscillator.frequency.value = 600;
|
||||||
|
const now = ctx.currentTime;
|
||||||
|
gain.gain.setValueAtTime(0, now);
|
||||||
|
gain.gain.linearRampToValueAtTime(0.3, now + 0.02);
|
||||||
|
gain.gain.linearRampToValueAtTime(0, now + 0.15);
|
||||||
|
oscillator.start(now);
|
||||||
|
oscillator.stop(now + 0.15);
|
||||||
|
oscillator.onended = () => ctx.close();
|
||||||
|
} catch {
|
||||||
|
// Audio blocked before first user interaction — fail silently
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only allow window.open for URLs whose origin matches the current app origin.
|
* Only allow window.open for URLs whose origin matches the current app origin.
|
||||||
* External-looking URLs (different host or protocol-relative) are rejected to
|
* External-looking URLs (different host or protocol-relative) are rejected to
|
||||||
@@ -83,6 +104,9 @@ const NotificationBell: React.FC = () => {
|
|||||||
|
|
||||||
// Find notifications we haven't seen before
|
// Find notifications we haven't seen before
|
||||||
const newOnes = unread.filter((n) => !knownIdsRef.current!.has(n.id));
|
const newOnes = unread.filter((n) => !knownIdsRef.current!.has(n.id));
|
||||||
|
if (newOnes.length > 0) {
|
||||||
|
playNotificationSound();
|
||||||
|
}
|
||||||
newOnes.forEach((n) => {
|
newOnes.forEach((n) => {
|
||||||
knownIdsRef.current!.add(n.id);
|
knownIdsRef.current!.add(n.id);
|
||||||
const severity = n.schwere === 'fehler' ? 'error' : n.schwere === 'warnung' ? 'warning' : 'info';
|
const severity = n.schwere === 'fehler' ? 'error' : n.schwere === 'warnung' ? 'warning' : 'info';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useCallback, useEffect, useRef, ReactNode } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { nextcloudApi } from '../services/nextcloud';
|
import { nextcloudApi } from '../services/nextcloud';
|
||||||
import { useLayout } from './LayoutContext';
|
import { useLayout } from './LayoutContext';
|
||||||
import type { NextcloudConversation } from '../types/nextcloud.types';
|
import type { NextcloudConversation } from '../types/nextcloud.types';
|
||||||
@@ -21,6 +21,17 @@ interface ChatProviderProps {
|
|||||||
export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
|
export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
|
||||||
const [selectedRoomToken, setSelectedRoomToken] = useState<string | null>(null);
|
const [selectedRoomToken, setSelectedRoomToken] = useState<string | null>(null);
|
||||||
const { chatPanelOpen } = useLayout();
|
const { chatPanelOpen } = useLayout();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const prevPanelOpenRef = useRef(chatPanelOpen);
|
||||||
|
|
||||||
|
// Invalidate rooms/connection when panel opens so data is fresh immediately
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatPanelOpen && !prevPanelOpenRef.current) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'connection'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] });
|
||||||
|
}
|
||||||
|
prevPanelOpenRef.current = chatPanelOpen;
|
||||||
|
}, [chatPanelOpen, queryClient]);
|
||||||
|
|
||||||
const { data: connData } = useQuery({
|
const { data: connData } = useQuery({
|
||||||
queryKey: ['nextcloud', 'connection'],
|
queryKey: ['nextcloud', 'connection'],
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ export async function syncToDatabase(
|
|||||||
const profileResult = await client.query<{ user_id: string }>(
|
const profileResult = await client.query<{ user_id: string }>(
|
||||||
`SELECT mp.user_id
|
`SELECT mp.user_id
|
||||||
FROM mitglieder_profile mp
|
FROM mitglieder_profile mp
|
||||||
WHERE mp.fdisk_standesbuch_nr = $1`,
|
JOIN users u ON u.id = mp.user_id
|
||||||
|
WHERE mp.fdisk_standesbuch_nr = $1
|
||||||
|
AND u.last_login_at IS NOT NULL`,
|
||||||
[member.standesbuchNr]
|
[member.standesbuchNr]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -78,18 +80,20 @@ export async function syncToDatabase(
|
|||||||
if (profileResult.rows.length > 0) {
|
if (profileResult.rows.length > 0) {
|
||||||
userId = profileResult.rows[0].user_id;
|
userId = profileResult.rows[0].user_id;
|
||||||
} else {
|
} else {
|
||||||
// Fallback: match by name (case-insensitive)
|
// Fallback: match by name (case-insensitive), only logged-in users
|
||||||
const nameResult = await client.query<{ id: string }>(
|
const nameResult = await client.query<{ id: string }>(
|
||||||
`SELECT u.id
|
`SELECT u.id
|
||||||
FROM users u
|
FROM users u
|
||||||
JOIN mitglieder_profile mp ON mp.user_id = u.id
|
JOIN mitglieder_profile mp ON mp.user_id = u.id
|
||||||
WHERE LOWER(u.given_name) = LOWER($1)
|
WHERE LOWER(u.given_name) = LOWER($1)
|
||||||
AND LOWER(u.family_name) = LOWER($2)
|
AND LOWER(u.family_name) = LOWER($2)
|
||||||
LIMIT 1`,
|
AND u.last_login_at IS NOT NULL`,
|
||||||
[member.vorname, member.zuname]
|
[member.vorname, member.zuname]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nameResult.rows.length > 0) {
|
if (nameResult.rows.length > 1) {
|
||||||
|
log(`WARN: skipping ${member.vorname} ${member.zuname} (Standesbuch-Nr ${member.standesbuchNr}) — duplicate name match (${nameResult.rows.length} users)`);
|
||||||
|
} else if (nameResult.rows.length === 1) {
|
||||||
userId = nameResult.rows[0].id;
|
userId = nameResult.rows[0].id;
|
||||||
// Store the Standesbuch-Nr now that we found a match
|
// Store the Standesbuch-Nr now that we found a match
|
||||||
await client.query(
|
await client.query(
|
||||||
|
|||||||
Reference in New Issue
Block a user