(frontend) add user setting to disable idle disconnect feature

Allow users to opt-out of idle participant disconnection despite
default enforcement, trusting power users who modify this setting
won't forget to disconnect, though accepting risk they may block
maintenance configuration updates.
This commit is contained in:
lebaudantoine
2025-10-16 19:35:21 +02:00
committed by aleb_the_flash
parent 39be4697b0
commit dbc66c2f07
7 changed files with 66 additions and 2 deletions

View File

@@ -8,6 +8,8 @@ import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalytics
import posthog from 'posthog-js' import posthog from 'posthog-js'
import { connectionObserverStore } from '@/stores/connectionObserver' import { connectionObserverStore } from '@/stores/connectionObserver'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
import { userPreferencesStore } from '@/stores/userPreferences'
import { useSnapshot } from 'valtio'
export const useConnectionObserver = () => { export const useConnectionObserver = () => {
const room = useRoomContext() const room = useRoomContext()
@@ -16,6 +18,8 @@ export const useConnectionObserver = () => {
const { data } = useConfig() const { data } = useConfig()
const isAnalyticsEnabled = useIsAnalyticsEnabled() const isAnalyticsEnabled = useIsAnalyticsEnabled()
const userPreferencesSnap = useSnapshot(userPreferencesStore)
const idleDisconnectModalTimeoutRef = useRef<ReturnType< const idleDisconnectModalTimeoutRef = useRef<ReturnType<
typeof setTimeout typeof setTimeout
> | null>(null) > | null>(null)
@@ -34,10 +38,11 @@ export const useConnectionObserver = () => {
idleDisconnectModalTimeoutRef.current = null idleDisconnectModalTimeoutRef.current = null
} }
const isEnabled = userPreferencesSnap.is_idle_disconnect_modal_enabled
const delay = data?.idle_disconnect_warning_delay const delay = data?.idle_disconnect_warning_delay
// Disabled or invalid delay: ensure modal is closed // Disabled or invalid delay: ensure modal is closed
if (!delay) { if (!isEnabled || !delay) {
connectionObserverStore.isIdleDisconnectModalOpen = false connectionObserverStore.isIdleDisconnectModalOpen = false
return return
} }
@@ -56,7 +61,11 @@ export const useConnectionObserver = () => {
idleDisconnectModalTimeoutRef.current = null idleDisconnectModalTimeoutRef.current = null
} }
} }
}, [remoteParticipants.length, data?.idle_disconnect_warning_delay]) }, [
remoteParticipants.length,
data?.idle_disconnect_warning_delay,
userPreferencesSnap.is_idle_disconnect_modal_enabled,
])
useEffect(() => { useEffect(() => {
if (!isAnalyticsEnabled) return if (!isAnalyticsEnabled) return

View File

@@ -2,6 +2,8 @@ import { Field, H } from '@/primitives'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useLanguageLabels } from '@/i18n/useLanguageLabels' import { useLanguageLabels } from '@/i18n/useLanguageLabels'
import { TabPanel, TabPanelProps } from '@/primitives/Tabs' import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import { userPreferencesStore } from '@/stores/userPreferences'
import { useSnapshot } from 'valtio'
export type GeneralTabProps = Pick<TabPanelProps, 'id'> export type GeneralTabProps = Pick<TabPanelProps, 'id'>
@@ -9,6 +11,8 @@ export const GeneralTab = ({ id }: GeneralTabProps) => {
const { t, i18n } = useTranslation('settings') const { t, i18n } = useTranslation('settings')
const { languagesList, currentLanguage } = useLanguageLabels() const { languagesList, currentLanguage } = useLanguageLabels()
const userPreferencesSnap = useSnapshot(userPreferencesStore)
return ( return (
<TabPanel padding={'md'} flex id={id}> <TabPanel padding={'md'} flex id={id}>
<H lvl={2}>{t('language.heading')}</H> <H lvl={2}>{t('language.heading')}</H>
@@ -21,6 +25,20 @@ export const GeneralTab = ({ id }: GeneralTabProps) => {
i18n.changeLanguage(lang as string) i18n.changeLanguage(lang as string)
}} }}
/> />
<H lvl={2}>{t('preferences.title')}</H>
<Field
type="switch"
label={t('preferences.idleDisconnectModal.label')}
description={t('preferences.idleDisconnectModal.description')}
isSelected={userPreferencesSnap.is_idle_disconnect_modal_enabled}
onChange={(value) =>
(userPreferencesStore.is_idle_disconnect_modal_enabled = value)
}
wrapperProps={{
noMargin: true,
fullWidth: true,
}}
/>
</TabPanel> </TabPanel>
) )
} }

View File

@@ -7,6 +7,13 @@
"authentication": "Authentifizierung", "authentication": "Authentifizierung",
"nameError": "Ihr Name darf nicht leer sein" "nameError": "Ihr Name darf nicht leer sein"
}, },
"preferences": {
"title": "Einstellungen",
"idleDisconnectModal": {
"label": "Anrufe ohne Teilnehmer verlassen",
"description": "Verlässt automatisch einen Anruf nach einigen Minuten, wenn kein anderer Teilnehmer beitritt"
}
},
"audio": { "audio": {
"microphone": { "microphone": {
"heading": "Mikrofon", "heading": "Mikrofon",

View File

@@ -7,6 +7,13 @@
"authentication": "Authentication", "authentication": "Authentication",
"nameError": "Your name cannot be empty" "nameError": "Your name cannot be empty"
}, },
"preferences": {
"title": "Preferences",
"idleDisconnectModal": {
"label": "Leave calls with no participants",
"description": "Automatically leaves a call after a few minutes if no other participant joins"
}
},
"audio": { "audio": {
"microphone": { "microphone": {
"heading": "Microphone", "heading": "Microphone",

View File

@@ -7,6 +7,13 @@
"authentication": "Authentification", "authentication": "Authentification",
"nameError": "Votre Nom ne peut pas être vide" "nameError": "Votre Nom ne peut pas être vide"
}, },
"preferences": {
"title": "Préférences",
"idleDisconnectModal": {
"label": "Quitter les appels sans autre participant",
"description": "Vous fait quitter un appel au bout de quelques minutes si aucun autre participant ne vous rejoint"
}
},
"audio": { "audio": {
"microphone": { "microphone": {
"heading": "Micro", "heading": "Micro",

View File

@@ -7,6 +7,13 @@
"authentication": "Authenticatie", "authentication": "Authenticatie",
"nameError": "Uw naam mag niet leeg zijn" "nameError": "Uw naam mag niet leeg zijn"
}, },
"preferences": {
"title": "Voorkeuren",
"idleDisconnectModal": {
"label": "Verlaat oproepen zonder deelnemers",
"description": "Verlaat automatisch een oproep na een paar minuten als geen andere deelnemer meedoet"
}
},
"audio": { "audio": {
"microphone": { "microphone": {
"heading": "Microfoon", "heading": "Microfoon",

View File

@@ -0,0 +1,9 @@
import { proxy } from 'valtio'
type State = {
is_idle_disconnect_modal_enabled: boolean
}
export const userPreferencesStore = proxy<State>({
is_idle_disconnect_modal_enabled: true,
})