feat(web): add composer enter behavior setting (#586)

This commit is contained in:
junes
2026-05-07 08:27:13 +08:00
committed by GitHub
parent 6df84df756
commit 08d3d9e111
9 changed files with 398 additions and 5 deletions
@@ -0,0 +1,32 @@
import { beforeEach, describe, expect, it } from 'vitest'
import {
DEFAULT_COMPOSER_ENTER_BEHAVIOR,
getComposerEnterBehaviorOptions,
getInitialComposerEnterBehavior,
} from './useComposerEnterBehavior'
describe('useComposerEnterBehavior helpers', () => {
beforeEach(() => {
window.localStorage.clear()
})
it('returns the allowed enter behavior options', () => {
expect(getComposerEnterBehaviorOptions()).toEqual([
{ value: 'send', labelKey: 'settings.chat.enterBehavior.send' },
{ value: 'newline', labelKey: 'settings.chat.enterBehavior.newline' },
])
})
it('falls back to the default behavior for missing or invalid storage values', () => {
expect(getInitialComposerEnterBehavior()).toBe(DEFAULT_COMPOSER_ENTER_BEHAVIOR)
window.localStorage.setItem('hapi-composer-enter-behavior', 'invalid')
expect(getInitialComposerEnterBehavior()).toBe(DEFAULT_COMPOSER_ENTER_BEHAVIOR)
})
it('reads a valid stored enter behavior', () => {
window.localStorage.setItem('hapi-composer-enter-behavior', 'newline')
expect(getInitialComposerEnterBehavior()).toBe('newline')
})
})
+99
View File
@@ -0,0 +1,99 @@
import { useCallback, useEffect, useState } from 'react'
export type ComposerEnterBehavior = 'send' | 'newline'
export const DEFAULT_COMPOSER_ENTER_BEHAVIOR: ComposerEnterBehavior = 'send'
export function getComposerEnterBehaviorOptions(): ReadonlyArray<{ value: ComposerEnterBehavior; labelKey: string }> {
return [
{ value: 'send', labelKey: 'settings.chat.enterBehavior.send' },
{ value: 'newline', labelKey: 'settings.chat.enterBehavior.newline' },
]
}
function getComposerEnterBehaviorStorageKey(): string {
return 'hapi-composer-enter-behavior'
}
function isBrowser(): boolean {
return typeof window !== 'undefined' && typeof document !== 'undefined'
}
function safeGetItem(key: string): string | null {
if (!isBrowser()) {
return null
}
try {
return localStorage.getItem(key)
} catch {
return null
}
}
function safeSetItem(key: string, value: string): void {
if (!isBrowser()) {
return
}
try {
localStorage.setItem(key, value)
} catch {
// Ignore storage errors
}
}
function safeRemoveItem(key: string): void {
if (!isBrowser()) {
return
}
try {
localStorage.removeItem(key)
} catch {
// Ignore storage errors
}
}
function parseComposerEnterBehavior(raw: string | null): ComposerEnterBehavior {
if (raw === 'send' || raw === 'newline') {
return raw
}
return DEFAULT_COMPOSER_ENTER_BEHAVIOR
}
export function getInitialComposerEnterBehavior(): ComposerEnterBehavior {
return parseComposerEnterBehavior(safeGetItem(getComposerEnterBehaviorStorageKey()))
}
export function useComposerEnterBehavior(): {
composerEnterBehavior: ComposerEnterBehavior
setComposerEnterBehavior: (behavior: ComposerEnterBehavior) => void
} {
const [composerEnterBehavior, setComposerEnterBehaviorState] = useState<ComposerEnterBehavior>(getInitialComposerEnterBehavior)
useEffect(() => {
if (!isBrowser()) {
return
}
const onStorage = (event: StorageEvent) => {
if (event.key !== getComposerEnterBehaviorStorageKey()) {
return
}
setComposerEnterBehaviorState(parseComposerEnterBehavior(event.newValue))
}
window.addEventListener('storage', onStorage)
return () => window.removeEventListener('storage', onStorage)
}, [])
const setComposerEnterBehavior = useCallback((behavior: ComposerEnterBehavior) => {
setComposerEnterBehaviorState(behavior)
if (behavior === DEFAULT_COMPOSER_ENTER_BEHAVIOR) {
safeRemoveItem(getComposerEnterBehaviorStorageKey())
} else {
safeSetItem(getComposerEnterBehaviorStorageKey(), behavior)
}
}, [])
return { composerEnterBehavior, setComposerEnterBehavior }
}