mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): keep push-to-talk release delays above 500 ms (#3453)
This commit is contained in:
1 parent
959b03b5f4
commit
f509bef1fb
7 files changed
+78
-17
No files matched your search
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {parseNumericInput, resolveNearestNumericOption} from '@app/features/ui/components/form/ComboboxNumericInput';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const RELEASE_DELAY_OPTIONS = [20, 100, 250, 500, 1000, 1500, 2000].map((value) => ({
|
||||
value,
|
||||
label: new Intl.NumberFormat('en-US', {style: 'unit', unit: 'millisecond', unitDisplay: 'narrow'}).format(value),
|
||||
}));
|
||||
|
||||
describe('parseNumericInput', () => {
|
||||
it('reads grouped thousands in common locale formats', () => {
|
||||
expect(parseNumericInput('1,000ms')).toBe(1000);
|
||||
expect(parseNumericInput('1.500 ms')).toBe(1500);
|
||||
expect(parseNumericInput('2 000 ms')).toBe(2000);
|
||||
expect(parseNumericInput("1'000")).toBe(1000);
|
||||
});
|
||||
|
||||
it('keeps decimals', () => {
|
||||
expect(parseNumericInput('1.5 s')).toBe(1.5);
|
||||
expect(parseNumericInput('1,5 s')).toBe(1.5);
|
||||
expect(parseNumericInput('250')).toBe(250);
|
||||
});
|
||||
|
||||
it('returns undefined without digits', () => {
|
||||
expect(parseNumericInput('ms')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveNearestNumericOption', () => {
|
||||
it('keeps release delays above 500 ms when the selected label is read back', () => {
|
||||
for (const option of RELEASE_DELAY_OPTIONS) {
|
||||
expect(resolveNearestNumericOption(option.label, RELEASE_DELAY_OPTIONS)).toBe(option.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
|
||||
|
||||
const DIGIT_GROUP_SEPARATOR = /(?<=\d)[,.'\u2019\s\u00a0\u202f](?=\d{3}(?!\d))/g;
|
||||
|
||||
export function parseNumericInput(inputValue: string): number | undefined {
|
||||
const numericMatch = inputValue
|
||||
.trim()
|
||||
.replace(DIGIT_GROUP_SEPARATOR, '')
|
||||
.match(/([0-9]+(?:[.,][0-9]+)?)/);
|
||||
if (!numericMatch) return undefined;
|
||||
const parsedValue = Number(numericMatch[1].replace(',', '.'));
|
||||
return Number.isFinite(parsedValue) ? parsedValue : undefined;
|
||||
}
|
||||
|
||||
export function resolveNearestNumericOption(
|
||||
inputValue: string,
|
||||
options: ReadonlyArray<ComboboxOption<number>>,
|
||||
): number | undefined {
|
||||
const parsedValue = parseNumericInput(inputValue);
|
||||
if (parsedValue === undefined) return undefined;
|
||||
return options.reduce((nearest, option) =>
|
||||
Math.abs(option.value - parsedValue) < Math.abs(nearest.value - parsedValue) ? option : nearest,
|
||||
).value;
|
||||
}
|
||||
@@ -21,17 +21,12 @@ export interface ComboboxOption<V extends Primitive = string> {
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export function resolveNearestNumericOption(
|
||||
function findOptionByLabel<V extends Primitive, O extends ComboboxOption<V>>(
|
||||
inputValue: string,
|
||||
options: ReadonlyArray<ComboboxOption<number>>,
|
||||
): number | undefined {
|
||||
const numericMatch = inputValue.trim().match(/([0-9]+(?:\.[0-9]+)?)/);
|
||||
if (!numericMatch) return undefined;
|
||||
const parsedValue = Number(numericMatch[1]);
|
||||
if (!Number.isFinite(parsedValue)) return undefined;
|
||||
return options.reduce((nearest, option) =>
|
||||
Math.abs(option.value - parsedValue) < Math.abs(nearest.value - parsedValue) ? option : nearest,
|
||||
).value;
|
||||
options: ReadonlyArray<O>,
|
||||
): O | undefined {
|
||||
const lowered = inputValue.trim().toLowerCase();
|
||||
return options.find((option) => option.label.trim().toLowerCase() === lowered);
|
||||
}
|
||||
|
||||
export interface ComboboxFilterOption<O> {
|
||||
@@ -197,6 +192,8 @@ export const Combobox = observer(function Combobox<
|
||||
);
|
||||
const resolveInputValue = useCallback(
|
||||
(inputValue: string): V | undefined => {
|
||||
const labelMatch = findOptionByLabel<V, O>(inputValue, options);
|
||||
if (labelMatch) return labelMatch.value;
|
||||
const customValue = autoSelectValueFromInput?.(inputValue, options);
|
||||
if (customValue !== undefined) return customValue;
|
||||
if (!autoSelectExactMatch) return undefined;
|
||||
|
||||
@@ -13,7 +13,8 @@ import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||
import {Message as MessageModel} from '@app/features/messaging/models/MessagingMessage';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import {type ComboboxOption, resolveNearestNumericOption} from '@app/features/ui/components/form/FormCombobox';
|
||||
import {resolveNearestNumericOption} from '@app/features/ui/components/form/ComboboxNumericInput';
|
||||
import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
|
||||
import {Switch} from '@app/features/ui/components/form/FormSwitch';
|
||||
import {MockAvatar} from '@app/features/ui/components/MockAvatar';
|
||||
import MobileLayout from '@app/features/ui/state/MobileLayout';
|
||||
|
||||
@@ -15,6 +15,7 @@ import NativePermission from '@app/features/permissions/system/state/NativePermi
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import {parseNumericInput} from '@app/features/ui/components/form/ComboboxNumericInput';
|
||||
import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
|
||||
import {Switch} from '@app/features/ui/components/form/FormSwitch';
|
||||
import {RESET_SLIDER_TO_DEFAULT_VALUE_DESCRIPTOR, Slider} from '@app/features/ui/components/Slider';
|
||||
@@ -161,10 +162,8 @@ const resolvePushToTalkReleaseDelayInput = (
|
||||
options: ReadonlyArray<ComboboxOption<number>>,
|
||||
): number | undefined => {
|
||||
const normalized = inputValue.trim().toLowerCase();
|
||||
const numericMatch = normalized.match(/([0-9]+(?:\.[0-9]+)?)/);
|
||||
if (!numericMatch) return undefined;
|
||||
const parsedValue = Number(numericMatch[1]);
|
||||
if (!Number.isFinite(parsedValue)) return undefined;
|
||||
const parsedValue = parseNumericInput(normalized);
|
||||
if (parsedValue === undefined) return undefined;
|
||||
const unitMatch = normalized.match(/[0-9.]\s*([a-z]+)/);
|
||||
const unit = unitMatch?.[1] ?? 'ms';
|
||||
const valueInMs = unit.startsWith('s') ? parsedValue * 1000 : parsedValue;
|
||||
|
||||
@@ -12,7 +12,8 @@ import Channels from '@app/features/channel/state/Channels';
|
||||
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||
import {Message as MessageModel} from '@app/features/messaging/models/MessagingMessage';
|
||||
import {isNewMessageGroup} from '@app/features/messaging/utils/MessageGroupingUtils';
|
||||
import {type ComboboxOption, resolveNearestNumericOption} from '@app/features/ui/components/form/FormCombobox';
|
||||
import {resolveNearestNumericOption} from '@app/features/ui/components/form/ComboboxNumericInput';
|
||||
import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
|
||||
import {Switch} from '@app/features/ui/components/form/FormSwitch';
|
||||
import {canResetSliderValue, SliderResetIconButton} from '@app/features/ui/components/slider/SliderResetIconButton';
|
||||
import type {RadioOption} from '@app/features/ui/radio_group/RadioGroup';
|
||||
|
||||
@@ -8,7 +8,8 @@ import Accessibility, {
|
||||
} from '@app/features/accessibility/state/Accessibility';
|
||||
import {APP_ZOOM_LEVEL_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||
import {type ComboboxOption, resolveNearestNumericOption} from '@app/features/ui/components/form/FormCombobox';
|
||||
import {resolveNearestNumericOption} from '@app/features/ui/components/form/ComboboxNumericInput';
|
||||
import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
|
||||
import {Slider} from '@app/features/ui/components/Slider';
|
||||
import {canResetSliderValue, SliderResetIconButton} from '@app/features/ui/components/slider/SliderResetIconButton';
|
||||
import {formatRoundedPercentage, roundPercentage} from '@app/features/ui/utils/PercentageFormatting';
|
||||
|
||||
Reference in new issue
Block a user