mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(privacy): let minors block media in DMs from others (#3120)
This commit is contained in:
@@ -226,11 +226,17 @@ export class UserAccountSettingsService {
|
||||
updatedRowData.sensitive_content_friend_dm_filter = data.sensitive_content_friend_dm_filter;
|
||||
}
|
||||
if (data.sensitive_content_non_friend_dm_filter !== undefined) {
|
||||
throw ValidationError.fromPath(
|
||||
'sensitive_content_non_friend_dm_filter',
|
||||
'AGE_RESTRICTED',
|
||||
'Non-adult users cannot modify the non-friend DM content filter',
|
||||
);
|
||||
const allowed =
|
||||
data.sensitive_content_non_friend_dm_filter === SensitiveMediaFilterLevel.BLUR ||
|
||||
data.sensitive_content_non_friend_dm_filter === SensitiveMediaFilterLevel.BLOCK;
|
||||
if (!allowed) {
|
||||
throw ValidationError.fromPath(
|
||||
'sensitive_content_non_friend_dm_filter',
|
||||
'AGE_RESTRICTED',
|
||||
'Non-adult users can only set non-friend DM filter to blur or block',
|
||||
);
|
||||
}
|
||||
updatedRowData.sensitive_content_non_friend_dm_filter = data.sensitive_content_non_friend_dm_filter;
|
||||
}
|
||||
if (data.sensitive_content_guild_filter !== undefined) {
|
||||
throw ValidationError.fromPath(
|
||||
|
||||
@@ -100,6 +100,35 @@ describe('User Settings - Sensitive Content Filters', () => {
|
||||
expect(response.errors[0]?.path).toBe('sensitive_content_friend_dm_filter');
|
||||
expect(response.errors[0]?.code).toBe(ValidationErrorCodes.AGE_RESTRICTED);
|
||||
});
|
||||
test('lets a non-adult account tighten the non-friend DM filter', async () => {
|
||||
const account = await createTestAccount(harness, {dateOfBirth: '2010-01-01'});
|
||||
const {json} = await updateUserSettings(harness, account.token, {
|
||||
sensitive_content_non_friend_dm_filter: SensitiveMediaFilterLevel.BLUR,
|
||||
});
|
||||
expect(json.sensitive_content_non_friend_dm_filter).toBe(SensitiveMediaFilterLevel.BLUR);
|
||||
const {json: blocked} = await updateUserSettings(harness, account.token, {
|
||||
sensitive_content_non_friend_dm_filter: SensitiveMediaFilterLevel.BLOCK,
|
||||
});
|
||||
expect(blocked.sensitive_content_non_friend_dm_filter).toBe(SensitiveMediaFilterLevel.BLOCK);
|
||||
});
|
||||
test('rejects a non-adult account relaxing the non-friend DM filter', async () => {
|
||||
const account = await createTestAccount(harness, {dateOfBirth: '2010-01-01'});
|
||||
const response = await createBuilder<{
|
||||
code: string;
|
||||
errors: Array<{
|
||||
path: string;
|
||||
code: string;
|
||||
message: string;
|
||||
}>;
|
||||
}>(harness, account.token)
|
||||
.patch('/users/@me/settings')
|
||||
.body({sensitive_content_non_friend_dm_filter: SensitiveMediaFilterLevel.SHOW})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
expect(response.code).toBe('VALIDATION_ERROR');
|
||||
expect(response.errors[0]?.path).toBe('sensitive_content_non_friend_dm_filter');
|
||||
expect(response.errors[0]?.code).toBe(ValidationErrorCodes.AGE_RESTRICTED);
|
||||
});
|
||||
test('allows an account without a birth date to relax every filter when none is collected', async () => {
|
||||
const repository = getInstanceConfigRepository();
|
||||
const previous = (await repository.getAppPublicConfig()).registration.collect_date_of_birth;
|
||||
|
||||
+18
-16
@@ -51,6 +51,7 @@ const SENSITIVE_CONTENT_TAB_ID = 'privacy_safety';
|
||||
interface SensitiveContentOption {
|
||||
value: number;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface SensitiveContentChoiceRowProps {
|
||||
@@ -73,15 +74,16 @@ const SensitiveContentChoiceRow: React.FC<SensitiveContentChoiceRowProps> = ({
|
||||
const labelId = useId();
|
||||
const optionRefs = useRef(new Map<number, HTMLButtonElement>());
|
||||
const selectedIndex = options.findIndex((option) => option.value === value);
|
||||
const focusedIndex = selectedIndex >= 0 ? selectedIndex : 0;
|
||||
const enabledOptions = options.filter((option) => !option.disabled);
|
||||
const focusedValue = enabledOptions.some((option) => option.value === value) ? value : enabledOptions[0]?.value;
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, optionValue: number) => {
|
||||
if (disabled) return;
|
||||
const currentIndex = options.findIndex((option) => option.value === optionValue);
|
||||
const currentIndex = enabledOptions.findIndex((option) => option.value === optionValue);
|
||||
if (currentIndex < 0) return;
|
||||
const direction = getTabNavigationDirection(event.key, 'horizontal');
|
||||
if (!direction) return;
|
||||
const nextIndex = getNextTabIndex(currentIndex, options.length, direction);
|
||||
const nextOption = nextIndex == null ? null : options[nextIndex];
|
||||
const nextIndex = getNextTabIndex(currentIndex, enabledOptions.length, direction);
|
||||
const nextOption = nextIndex == null ? null : enabledOptions[nextIndex];
|
||||
if (!nextOption) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -101,7 +103,7 @@ const SensitiveContentChoiceRow: React.FC<SensitiveContentChoiceRowProps> = ({
|
||||
aria-disabled={disabled || undefined}
|
||||
data-flx={dataFlx}
|
||||
>
|
||||
{options.map((option, index) => {
|
||||
{options.map((option) => {
|
||||
const isSelected = option.value === value;
|
||||
return (
|
||||
<button
|
||||
@@ -116,8 +118,8 @@ const SensitiveContentChoiceRow: React.FC<SensitiveContentChoiceRowProps> = ({
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={isSelected}
|
||||
tabIndex={!disabled && index === focusedIndex ? 0 : -1}
|
||||
disabled={disabled}
|
||||
tabIndex={!disabled && option.value === focusedValue ? 0 : -1}
|
||||
disabled={disabled || option.disabled}
|
||||
className={clsx(styles.choiceButton, isSelected && styles.choiceButtonActive)}
|
||||
onClick={() => onChange(option.value)}
|
||||
onKeyDown={(event) => handleKeyDown(event, option.value)}
|
||||
@@ -161,11 +163,10 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
|
||||
const [nonFriendDmFilter, setNonFriendDmFilter] = useState(UserSettings.sensitiveContentNonFriendDmFilter);
|
||||
const [guildFilter, setGuildFilter] = useState(UserSettings.sensitiveContentGuildFilter);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const hasUnsavedChanges = isMatureContentAllowed
|
||||
? friendDmFilter !== UserSettings.sensitiveContentFriendDmFilter ||
|
||||
nonFriendDmFilter !== UserSettings.sensitiveContentNonFriendDmFilter ||
|
||||
guildFilter !== UserSettings.sensitiveContentGuildFilter
|
||||
: friendDmFilter !== UserSettings.sensitiveContentFriendDmFilter;
|
||||
const hasUnsavedChanges =
|
||||
friendDmFilter !== UserSettings.sensitiveContentFriendDmFilter ||
|
||||
nonFriendDmFilter !== UserSettings.sensitiveContentNonFriendDmFilter ||
|
||||
(isMatureContentAllowed && guildFilter !== UserSettings.sensitiveContentGuildFilter);
|
||||
const handleReset = useCallback(() => {
|
||||
setFriendDmFilter(UserSettings.sensitiveContentFriendDmFilter);
|
||||
setNonFriendDmFilter(UserSettings.sensitiveContentNonFriendDmFilter);
|
||||
@@ -183,6 +184,7 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
|
||||
} else {
|
||||
await UserSettingsCommands.update({
|
||||
sensitiveContentFriendDmFilter: friendDmFilter,
|
||||
sensitiveContentNonFriendDmFilter: nonFriendDmFilter,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
@@ -212,8 +214,9 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
|
||||
],
|
||||
[i18n.locale],
|
||||
);
|
||||
const teenFriendDmOptions = useMemo(
|
||||
const teenDmOptions = useMemo(
|
||||
() => [
|
||||
{value: SensitiveMediaFilterLevel.SHOW, label: i18n._(SHOW_DESCRIPTOR), disabled: true},
|
||||
{value: SensitiveMediaFilterLevel.BLUR, label: i18n._(BLUR_DESCRIPTOR)},
|
||||
{value: SensitiveMediaFilterLevel.BLOCK, label: i18n._(BLOCK_DESCRIPTOR)},
|
||||
],
|
||||
@@ -234,7 +237,7 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
|
||||
<SensitiveContentChoiceRow
|
||||
label={i18n._(DIRECT_MESSAGES_FROM_FRIENDS_DESCRIPTOR)}
|
||||
value={friendDmFilter}
|
||||
options={isMatureContentAllowed ? filterOptions : teenFriendDmOptions}
|
||||
options={isMatureContentAllowed ? filterOptions : teenDmOptions}
|
||||
onChange={setFriendDmFilter}
|
||||
dataFlx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.select.set-friend-dm-filter"
|
||||
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.sensitive-content-choice-row.set-friend-dm-filter"
|
||||
@@ -242,9 +245,8 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
|
||||
<SensitiveContentChoiceRow
|
||||
label={i18n._(DIRECT_MESSAGES_FROM_OTHERS_DESCRIPTOR)}
|
||||
value={nonFriendDmFilter}
|
||||
options={filterOptions}
|
||||
options={isMatureContentAllowed ? filterOptions : teenDmOptions}
|
||||
onChange={setNonFriendDmFilter}
|
||||
disabled={!isMatureContentAllowed}
|
||||
dataFlx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.select.set-non-friend-dm-filter"
|
||||
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.sensitive-content-choice-row.set-non-friend-dm-filter"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user