mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-10 20:52:12 +09:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a208b56354 | ||
|
|
71be6c1de4 | ||
|
|
eae905b1da | ||
|
|
10cd4f96a4 | ||
|
|
9d74daf8ad | ||
|
|
6f344def93 | ||
|
|
72f9f9c396 | ||
|
|
c8755885d4 | ||
|
|
12d503d4c5 | ||
|
|
2f6201bcbd | ||
|
|
ec681e6061 | ||
|
|
0fc6fad11d | ||
|
|
eb329f2cd0 | ||
|
|
9a20821332 | ||
|
|
4749eb7f86 | ||
|
|
2d83fd2a93 | ||
|
|
d180af6ba2 | ||
|
|
4810eb3e24 | ||
|
|
48a19dedd8 | ||
|
|
cadf7ea532 | ||
|
|
fc60d05f17 | ||
|
|
5072488f1e | ||
|
|
7a5b9a0ded | ||
|
|
4ce4d4e09a |
@@ -551,6 +551,16 @@ jobs:
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step verify_windows_unpacked_signatures
|
||||
|
||||
- name: Verify the packaged shell starts and boots its bundled renderer (Windows)
|
||||
if: matrix.platform == 'windows' && matrix.arch == 'x64'
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
env:
|
||||
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
|
||||
BUILD_VERSION: ${{ env.VERSION }}
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step verify_bundled_renderer_windows
|
||||
|
||||
- name: Create portable ZIP (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
@@ -591,6 +601,16 @@ jobs:
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step build_app_linux
|
||||
|
||||
- name: Verify the packaged shell boots its bundled renderer (Linux)
|
||||
if: matrix.platform == 'linux' && matrix.arch == 'x64'
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
env:
|
||||
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
|
||||
BUILD_VERSION: ${{ env.VERSION }}
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step verify_bundled_renderer_linux
|
||||
|
||||
- name: Verify signed Windows artifacts
|
||||
if: matrix.platform == 'windows'
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
@@ -651,13 +671,59 @@ jobs:
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
|
||||
verify_windows_arm64:
|
||||
name: Verify windows (arm64)
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' }}
|
||||
needs:
|
||||
- meta
|
||||
- build
|
||||
runs-on: windows-11-arm
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
env:
|
||||
CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||
VERSION: ${{ needs.meta.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_sha }}
|
||||
sparse-checkout: fluxer_desktop/scripts
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Download windows arm64 build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: artifacts
|
||||
pattern: fluxer-desktop-${{ needs.meta.outputs.build_channel }}-windows-arm64*
|
||||
|
||||
- name: Verify the packaged shell starts and boots its bundled renderer
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$zip = Get-ChildItem -Path artifacts -Recurse -Filter "*-$env:VERSION-portable-win-arm64.zip" | Select-Object -First 1
|
||||
if ($null -eq $zip) { Write-Host 'This build has no windows arm64 portable zip'; exit 0 }
|
||||
$app = Join-Path $env:RUNNER_TEMP 'app'
|
||||
Expand-Archive -Path $zip.FullName -DestinationPath $app
|
||||
$exe = Get-ChildItem -Path $app -Recurse -Filter 'Fluxer*.exe' | Where-Object { $_.Name -notlike '*_ExecutionStub.exe' } | Select-Object -First 1
|
||||
if ($null -eq $exe) { throw "$($zip.Name) holds no Fluxer executable" }
|
||||
node fluxer_desktop/scripts/release-check.mjs --app $exe.DirectoryName --channel $env:CHANNEL --expect-renderer $env:VERSION --expect-source bundled --package-origin http://127.0.0.1:9 --app-arg=--disable-gpu --workdir (Join-Path $env:RUNNER_TEMP 'runs') --timeout-seconds 240
|
||||
if ($LASTEXITCODE -ne 0) { throw "The windows arm64 build fails its release check" }
|
||||
|
||||
upload:
|
||||
name: Assemble desktop release assets
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' }}
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' && needs.verify_windows_arm64.result == 'success' }}
|
||||
needs:
|
||||
- meta
|
||||
- shared_assets
|
||||
- build
|
||||
- verify_windows_arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
environment: desktop-releases
|
||||
timeout-minutes: 180
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
name: desktop windows release check
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: Release channel
|
||||
type: choice
|
||||
options:
|
||||
- canary
|
||||
- stable
|
||||
default: canary
|
||||
version:
|
||||
description: Published version to check
|
||||
required: true
|
||||
type: string
|
||||
expect_failure:
|
||||
description: Pass only when the published build fails to start
|
||||
type: boolean
|
||||
default: false
|
||||
update_from:
|
||||
description: Comma-separated published versions to install with Setup and update to the version above from the live feed
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
stale_apply_record:
|
||||
description: Before each update, leave behind an hour-old record of an update that never installed (passes only when the installed versions retry it)
|
||||
type: boolean
|
||||
default: false
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/desktop-windows-release-check.yaml
|
||||
- fluxer_desktop/scripts/release-check.mjs
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: desktop-windows-release-check-${{ github.ref }}-${{ inputs.version || 'pr' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check windows (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
os: windows-2025
|
||||
- arch: arm64
|
||||
os: windows-11-arm
|
||||
env:
|
||||
CHANNEL: ${{ inputs.channel || 'canary' }}
|
||||
VERSION: ${{ inputs.version || '2026.1009.20411' }}
|
||||
EXPECT_FAILURE: ${{ github.event_name == 'pull_request' && 'true' || inputs.expect_failure }}
|
||||
UPDATE_FROM: ${{ inputs.update_from || '' }}
|
||||
STALE_APPLY_RECORD: ${{ inputs.stale_apply_record && 'true' || 'false' }}
|
||||
ARCH: ${{ matrix.arch }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
sparse-checkout: fluxer_desktop/scripts
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Check the portable build
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$work = Join-Path $env:RUNNER_TEMP 'portable'
|
||||
New-Item -ItemType Directory -Force -Path $work | Out-Null
|
||||
$zip = Join-Path $work 'portable.zip'
|
||||
Invoke-WebRequest -Uri "https://pkgs.fluxer.com/desktop/$env:CHANNEL/win32/$env:ARCH/$env:VERSION/portable" -OutFile $zip
|
||||
Expand-Archive -Path $zip -DestinationPath (Join-Path $work 'app')
|
||||
$exe = Get-ChildItem -Path (Join-Path $work 'app') -Recurse -Filter 'Fluxer*.exe' | Where-Object { $_.Name -notlike '*_ExecutionStub.exe' } | Select-Object -First 1
|
||||
if ($null -eq $exe) { throw "The portable build holds no Fluxer executable" }
|
||||
$output = & node fluxer_desktop/scripts/release-check.mjs --app $exe.DirectoryName --channel $env:CHANNEL --expect-renderer $env:VERSION --expect-source bundled --app-arg=--disable-gpu --workdir (Join-Path $work 'runs') --timeout-seconds 240 2>&1
|
||||
$code = $LASTEXITCODE
|
||||
$output | ForEach-Object { Write-Host $_ }
|
||||
if ($env:EXPECT_FAILURE -eq 'true') {
|
||||
if ($code -eq 0) { throw "Expected $env:VERSION to fail its release check, it passed" }
|
||||
Write-Host "$env:VERSION fails its release check on windows $env:ARCH as expected"
|
||||
exit 0
|
||||
}
|
||||
if ($code -ne 0) { throw "The release check failed for $env:VERSION on windows $env:ARCH" }
|
||||
|
||||
- name: Update installed builds from the live feed
|
||||
if: env.UPDATE_FROM != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$channelDirs = @{ canary = 'fluxer_desktop_canary'; stable = 'fluxer_desktop' }
|
||||
$dataDirs = @{ canary = 'fluxercanary'; stable = 'fluxer' }
|
||||
$installRoot = Join-Path $env:LOCALAPPDATA $channelDirs[$env:CHANNEL]
|
||||
$logPath = Join-Path (Join-Path $env:APPDATA $dataDirs[$env:CHANNEL]) 'logs\main.log'
|
||||
$failures = @()
|
||||
foreach ($from in ($env:UPDATE_FROM -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })) {
|
||||
Write-Host "== install $from, then update to $env:VERSION"
|
||||
Get-Process | Where-Object { $_.Path -like "$installRoot*" } | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $installRoot) { Remove-Item -Recurse -Force $installRoot }
|
||||
Remove-Item -Recurse -Force (Split-Path (Split-Path $logPath -Parent) -Parent) -ErrorAction SilentlyContinue
|
||||
$setup = Join-Path $env:RUNNER_TEMP "setup-$from.exe"
|
||||
Invoke-WebRequest -Uri "https://pkgs.fluxer.com/desktop/$env:CHANNEL/win32/$env:ARCH/$from/setup" -OutFile $setup
|
||||
Start-Process -FilePath $setup -ArgumentList '--silent' -Wait
|
||||
Start-Sleep -Seconds 5
|
||||
Get-Process | Where-Object { $_.Path -like "$installRoot*" } | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
$exe = Get-ChildItem -Path (Join-Path $installRoot 'current') -Filter 'Fluxer*.exe' | Where-Object { $_.Name -notlike '*_ExecutionStub.exe' } | Select-Object -First 1
|
||||
if ($null -eq $exe) { $failures += "$from did not install"; continue }
|
||||
if ($env:STALE_APPLY_RECORD -eq 'true') {
|
||||
$dataRoot = Split-Path (Split-Path $logPath -Parent) -Parent
|
||||
New-Item -ItemType Directory -Force -Path $dataRoot | Out-Null
|
||||
$attemptedAt = [DateTimeOffset]::UtcNow.AddHours(-1).ToUnixTimeMilliseconds()
|
||||
Set-Content -Path (Join-Path $dataRoot 'update-apply-state.json') -Value "{`"version`":`"$env:VERSION`",`"attemptedAt`":$attemptedAt}" -NoNewline
|
||||
Write-Host "left a stale apply record for $env:VERSION"
|
||||
}
|
||||
Start-Process -FilePath $exe.FullName -ArgumentList '--disable-gpu'
|
||||
$deadline = (Get-Date).AddMinutes(8)
|
||||
$reported = $null
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Seconds 10
|
||||
if (Test-Path $logPath) {
|
||||
$line = Select-String -Path $logPath -Pattern "The renderer reported its build: .*Desktop $([regex]::Escape($env:VERSION)), Web $([regex]::Escape($env:VERSION))" | Select-Object -Last 1
|
||||
if ($null -ne $line) { $reported = $line.Line; break }
|
||||
}
|
||||
}
|
||||
$installed = Get-Content (Join-Path $installRoot 'current\sq.version') -Raw -ErrorAction SilentlyContinue
|
||||
Write-Host "installed package after update: $installed"
|
||||
$logs = Join-Path $env:RUNNER_TEMP "logs\$from"
|
||||
New-Item -ItemType Directory -Force -Path $logs | Out-Null
|
||||
if (Test-Path $logPath) { Copy-Item $logPath $logs }
|
||||
Get-ChildItem -Path $installRoot -Filter '*.log' -ErrorAction SilentlyContinue | Copy-Item -Destination $logs
|
||||
if (Test-Path $logPath) { Get-Content $logPath | Select-String -Pattern 'Bootstrap|ShellSelfUpdate|Velopack|velopack|NativeModulePreflight|reported its build' | Select-Object -Last 40 | ForEach-Object { Write-Host $_.Line } }
|
||||
if ($null -eq $reported) { $failures += "$from did not reach Desktop and Web $env:VERSION" } else { Write-Host "$from updated: $reported" }
|
||||
Get-Process | Where-Object { $_.Path -like "$installRoot*" } | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($failures.Count -gt 0) { throw ($failures -join "`n") }
|
||||
|
||||
- name: Upload update logs
|
||||
if: always() && env.UPDATE_FROM != ''
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: update-logs-${{ matrix.arch }}
|
||||
path: ${{ runner.temp }}/logs
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
@@ -17262,6 +17262,7 @@
|
||||
"MAX_GROUP_DM_RECIPIENTS",
|
||||
"MAX_GROUP_DMS",
|
||||
"GROUP_DM_RECIPIENTS_NOT_ADDABLE",
|
||||
"GROUP_DM_MATURE_CONTENT_INELIGIBLE",
|
||||
"MAX_GUILD_CHANNELS",
|
||||
"MAX_GUILD_MEMBERS",
|
||||
"MAX_GUILD_ROLES",
|
||||
@@ -17558,6 +17559,7 @@
|
||||
"You've reached the maximum of {count, plural, one {# group DM recipient} other {# group DM recipients}}.",
|
||||
"You've reached the maximum of {count, plural, one {# group DM} other {# group DMs}}.",
|
||||
"One or more selected users can't be added to this group DM.",
|
||||
"Not everyone in this group is eligible for mature content.",
|
||||
"You've reached the maximum of {count, plural, one {# community channel} other {# community channels}}.",
|
||||
"You've reached the maximum of {count, plural, one {# community member} other {# community members}}.",
|
||||
"You've reached the maximum of {count, plural, one {# community role} other {# community roles}}.",
|
||||
|
||||
@@ -130,6 +130,9 @@ describe('DELETE /admin/reports/:report_id', () => {
|
||||
const profileKey = `reports/${reportId}/profile/user_avatar/abc${id}`;
|
||||
await harness.storageService.uploadObject({bucket: REPORTS_BUCKET, key: attachmentKey, body: new Uint8Array([1])});
|
||||
await harness.storageService.uploadObject({bucket: REPORTS_BUCKET, key: profileKey, body: new Uint8Array([2])});
|
||||
const uploadedAt = new Date(Date.now() - ms('1 hour'));
|
||||
harness.storageService.setObjectLastModified(REPORTS_BUCKET, attachmentKey, uploadedAt);
|
||||
harness.storageService.setObjectLastModified(REPORTS_BUCKET, profileKey, uploadedAt);
|
||||
const report = await repository.createReport({
|
||||
...Object.fromEntries(IAR_SUBMISSION_COLUMNS.map((column) => [column, null])),
|
||||
report_id: id,
|
||||
|
||||
@@ -165,6 +165,7 @@ function serializeGroupDMChannel(channel: Channel): ChannelResponse {
|
||||
icon: channel.iconHash ?? null,
|
||||
owner_id: channel.ownerId ? channel.ownerId.toString() : null,
|
||||
nicks: nicknameMap.size > 0 ? nicks : undefined,
|
||||
nsfw: channel.isNsfw,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ export class ChannelDataService {
|
||||
icon: groupDmData.icon !== undefined ? groupDmData.icon : undefined,
|
||||
ownerId: groupDmData.owner_id ? createUserID(groupDmData.owner_id) : undefined,
|
||||
nicks: groupDmData.nicks,
|
||||
nsfw: groupDmData.nsfw ?? undefined,
|
||||
requestCache,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'
|
||||
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
|
||||
import type {Channel} from '@app/api/models/Channel';
|
||||
import type {IUserRepository} from '@app/api/user/IUserRepository';
|
||||
import {canUserAccessNsfwContent} from '@app/api/utils/AgeUtils';
|
||||
import {ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {GroupDmMatureContentIneligibleError} from '@fluxer/errors/src/domains/channel/GroupDmMatureContentIneligibleError';
|
||||
import {InvalidChannelTypeError} from '@fluxer/errors/src/domains/channel/InvalidChannelTypeError';
|
||||
import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownChannelError';
|
||||
import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError';
|
||||
@@ -37,6 +39,7 @@ export class GroupDmUpdateService {
|
||||
icon,
|
||||
ownerId,
|
||||
nicks,
|
||||
nsfw,
|
||||
requestCache,
|
||||
}: {
|
||||
userId: UserID;
|
||||
@@ -45,6 +48,7 @@ export class GroupDmUpdateService {
|
||||
icon?: string | null;
|
||||
ownerId?: UserID;
|
||||
nicks?: Record<string, string | null> | null;
|
||||
nsfw?: boolean;
|
||||
requestCache: RequestCache;
|
||||
}): Promise<Channel> {
|
||||
const channel = await this.channelRepository.channelData.findUnique(channelId);
|
||||
@@ -113,6 +117,20 @@ export class GroupDmUpdateService {
|
||||
updates.nicks = updatedNicknames.size > 0 ? (updatedNicknames as Map<string, string>) : null;
|
||||
}
|
||||
}
|
||||
if (nsfw !== undefined && nsfw !== channel.isNsfw) {
|
||||
if (channel.ownerId !== userId) {
|
||||
throw new MissingPermissionsError();
|
||||
}
|
||||
if (nsfw) {
|
||||
const recipients = await Promise.all(
|
||||
Array.from(channel.recipientIds).map((id) => this.userRepository.findUnique(id)),
|
||||
);
|
||||
if (recipients.some((recipient) => !recipient || !canUserAccessNsfwContent(recipient))) {
|
||||
throw new GroupDmMatureContentIneligibleError();
|
||||
}
|
||||
}
|
||||
updates.nsfw = nsfw;
|
||||
}
|
||||
let iconHash: string | null = null;
|
||||
if (icon !== undefined) {
|
||||
iconHash = await this.avatarService.uploadAvatar({
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {User} from '@app/api/models/User';
|
||||
import {deleteChannelMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup';
|
||||
import {assertAccountNotLimited} from '@app/api/user/AccountLimit';
|
||||
import type {IUserRepository} from '@app/api/user/IUserRepository';
|
||||
import {canUserAccessNsfwContent} from '@app/api/utils/AgeUtils';
|
||||
import {UserPermissionUtils} from '@app/api/utils/UserPermissionUtils';
|
||||
import {ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
@@ -36,6 +37,7 @@ import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownCha
|
||||
import {CannotRemoveOtherRecipientsError} from '@fluxer/errors/src/domains/core/CannotRemoveOtherRecipientsError';
|
||||
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
|
||||
import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError';
|
||||
import {NsfwContentRequiresAgeVerificationError} from '@fluxer/errors/src/domains/moderation/NsfwContentRequiresAgeVerificationError';
|
||||
import {NotFriendsWithUserError} from '@fluxer/errors/src/domains/user/NotFriendsWithUserError';
|
||||
|
||||
export class GroupDmOperationsService {
|
||||
@@ -84,6 +86,9 @@ export class GroupDmOperationsService {
|
||||
userId,
|
||||
targetId: recipientId,
|
||||
});
|
||||
if (channel.isNsfw && !(await this.canJoinMatureGroup(recipientId))) {
|
||||
throw new MissingAccessError();
|
||||
}
|
||||
const {channel: updatedChannel} = await this.addRecipientViaInviteWithResult({
|
||||
channelId,
|
||||
recipientId,
|
||||
@@ -122,6 +127,11 @@ export class GroupDmOperationsService {
|
||||
return updatedChannel;
|
||||
}
|
||||
|
||||
private async canJoinMatureGroup(userId: UserID): Promise<boolean> {
|
||||
const user = await this.userRepository.findUnique(userId);
|
||||
return user != null && canUserAccessNsfwContent(user);
|
||||
}
|
||||
|
||||
private async assertInviterNotLimited(userId: UserID): Promise<void> {
|
||||
const user = await this.userRepository.findUnique(userId);
|
||||
if (user) assertAccountNotLimited(user);
|
||||
@@ -169,6 +179,9 @@ export class GroupDmOperationsService {
|
||||
if (channel.recipientIds.has(recipientId)) {
|
||||
return {channel, recipientAdded: false};
|
||||
}
|
||||
if (channel.isNsfw && !(await this.canJoinMatureGroup(recipientId))) {
|
||||
throw new NsfwContentRequiresAgeVerificationError();
|
||||
}
|
||||
const inviterUser = inviterId ? await this.userRepository.findUnique(inviterId) : null;
|
||||
const fallbackLimit = MAX_GROUP_DM_RECIPIENTS;
|
||||
const recipientLimit = this.resolveLimitForUser(inviterUser ?? null, 'max_group_dm_recipients', fallbackLimit);
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as EmojiUtils from '@app/api/utils/EmojiUtils';
|
||||
import {ChannelTypes, GUILD_TEXT_BASED_CHANNEL_TYPES} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {GuildExplicitContentFilterTypes, GuildFeatures, GuildNSFWLevel} from '@fluxer/constants/src/GuildConstants';
|
||||
import {THREAD_ONLY_CHANNEL_TYPES} from '@fluxer/constants/src/ThreadConstants';
|
||||
import {SensitiveMediaFilterLevel} from '@fluxer/constants/src/UserConstants';
|
||||
import {RelationshipTypes, SensitiveMediaFilterLevel} from '@fluxer/constants/src/UserConstants';
|
||||
import type {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
||||
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||
|
||||
@@ -40,6 +40,30 @@ export class MessageContentService {
|
||||
});
|
||||
}
|
||||
|
||||
async resolveDmNsfwContext(channel: Channel, senderId: UserID): Promise<DmNsfwContext | undefined> {
|
||||
if (channel.type !== ChannelTypes.DM) {
|
||||
return undefined;
|
||||
}
|
||||
const recipientIds = Array.from(channel.recipientIds).filter((id) => id !== senderId);
|
||||
if (recipientIds.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
const recipientId = recipientIds[0];
|
||||
const [senderSettings, recipientSettings, friendship] = await Promise.all([
|
||||
this.userRepository.findSettings(senderId),
|
||||
this.userRepository.findSettings(recipientId),
|
||||
this.userRepository.getRelationship(senderId, recipientId, RelationshipTypes.FRIEND),
|
||||
]);
|
||||
const areFriends = friendship != null;
|
||||
const senderFilterLevel = areFriends
|
||||
? (senderSettings?.sensitiveContentFriendDmFilter ?? SensitiveMediaFilterLevel.SHOW)
|
||||
: (senderSettings?.sensitiveContentNonFriendDmFilter ?? SensitiveMediaFilterLevel.BLOCK);
|
||||
const recipientFilterLevel = areFriends
|
||||
? (recipientSettings?.sensitiveContentFriendDmFilter ?? SensitiveMediaFilterLevel.SHOW)
|
||||
: (recipientSettings?.sensitiveContentNonFriendDmFilter ?? SensitiveMediaFilterLevel.BLOCK);
|
||||
return {senderFilterLevel, recipientFilterLevel};
|
||||
}
|
||||
|
||||
isNSFWContentAllowed(params: {
|
||||
channel?: Channel;
|
||||
guild?: GuildResponse | null;
|
||||
@@ -61,6 +85,9 @@ export class MessageContentService {
|
||||
if (channel?.type === ChannelTypes.DM_PERSONAL_NOTES) {
|
||||
return true;
|
||||
}
|
||||
if (channel?.type === ChannelTypes.GROUP_DM && channel.isNsfw) {
|
||||
return true;
|
||||
}
|
||||
if (!guild) {
|
||||
if (dmNsfwContext) {
|
||||
const eitherBlocks =
|
||||
|
||||
@@ -175,6 +175,7 @@ export class MessageEditService {
|
||||
});
|
||||
}
|
||||
const isBugHunterBot = !!user?.isBot && (user.flags & UserFlags.BUG_HUNTER) !== 0n;
|
||||
const dmNsfwContext = guild ? undefined : await this.deps.persistenceService.resolveDmNsfwContext(channel, userId);
|
||||
const updateResult = await this.deps.messageWriteLock.withFreshMessage(channelId, messageId, async (fresh) => {
|
||||
if (!fresh) throw new UnknownMessageError();
|
||||
return this.deps.crosspostPropagation.withPublishedEditBudget({fresh, actor: 'author'}, () =>
|
||||
@@ -190,6 +191,7 @@ export class MessageEditService {
|
||||
isBot: user?.isBot,
|
||||
isBugHunterBot,
|
||||
locale: user?.locale,
|
||||
dmNsfwContext,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -158,6 +158,10 @@ export class MessagePersistenceService {
|
||||
return resolveNsfwScopeChannel(channel, (channelId) => this.channelRepository.channelData.findUnique(channelId));
|
||||
}
|
||||
|
||||
resolveDmNsfwContext(channel: Channel, senderId: UserID): Promise<DmNsfwContext | undefined> {
|
||||
return this.contentService.resolveDmNsfwContext(channel, senderId);
|
||||
}
|
||||
|
||||
getEmbedAttachmentResolver(): MessageEmbedAttachmentResolver {
|
||||
return this.embedAttachmentResolver;
|
||||
}
|
||||
@@ -402,6 +406,7 @@ export class MessagePersistenceService {
|
||||
isBot?: boolean;
|
||||
isBugHunterBot?: boolean;
|
||||
locale?: string | null;
|
||||
dmNsfwContext?: DmNsfwContext;
|
||||
}): Promise<UpdateMessageResult> {
|
||||
const {message, messageId, data, channel, guild, member} = params;
|
||||
if (message.messageSnapshots && message.messageSnapshots.length > 0) {
|
||||
@@ -412,6 +417,7 @@ export class MessagePersistenceService {
|
||||
guild,
|
||||
member,
|
||||
isBot: params.isBot,
|
||||
dmNsfwContext: params.dmNsfwContext,
|
||||
});
|
||||
const updatedRowData = {...message.toRow()};
|
||||
let hasChanges = false;
|
||||
|
||||
@@ -19,7 +19,6 @@ import type {AuthenticatedChannel} from '@app/api/channel/services/Authenticated
|
||||
import type {CrosspostPropagation} from '@app/api/channel/services/message/CrosspostPropagation';
|
||||
import {emitMessageCreated} from '@app/api/channel/services/message/MessageActivity';
|
||||
import type {MessageChannelAuthService} from '@app/api/channel/services/message/MessageChannelAuthService';
|
||||
import type {DmNsfwContext} from '@app/api/channel/services/message/MessageContentService';
|
||||
import type {MessageDispatchService} from '@app/api/channel/services/message/MessageDispatchService';
|
||||
import type {MessageEmbedAttachmentResolver} from '@app/api/channel/services/message/MessageEmbedAttachmentResolver';
|
||||
import {
|
||||
@@ -69,12 +68,7 @@ import {
|
||||
} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {GuildNSFWLevel, GuildOperations} from '@fluxer/constants/src/GuildConstants';
|
||||
import {threadWriteBlock} from '@fluxer/constants/src/ThreadPermissionUtils';
|
||||
import {
|
||||
DELETED_USER_ID,
|
||||
RelationshipTypes,
|
||||
SensitiveMediaFilterLevel,
|
||||
UserFlags,
|
||||
} from '@fluxer/constants/src/UserConstants';
|
||||
import {DELETED_USER_ID, UserFlags} from '@fluxer/constants/src/UserConstants';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownChannelError';
|
||||
import {UnknownMessageError} from '@fluxer/errors/src/domains/channel/UnknownMessageError';
|
||||
@@ -193,33 +187,6 @@ export class MessageSendService {
|
||||
return includeDefault ? {includeDefault} : null;
|
||||
}
|
||||
|
||||
private async buildDmNsfwContext(channel: Channel, senderId: UserID): Promise<DmNsfwContext | undefined> {
|
||||
if (channel.type === ChannelTypes.GROUP_DM || channel.type === ChannelTypes.DM_PERSONAL_NOTES) {
|
||||
return undefined;
|
||||
}
|
||||
if (channel.type !== ChannelTypes.DM) {
|
||||
return undefined;
|
||||
}
|
||||
const recipientIds = Array.from(channel.recipientIds).filter((id) => id !== senderId);
|
||||
if (recipientIds.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
const recipientId = recipientIds[0];
|
||||
const [senderSettings, recipientSettings, friendship] = await Promise.all([
|
||||
this.deps.userRepository.findSettings(senderId),
|
||||
this.deps.userRepository.findSettings(recipientId),
|
||||
this.deps.userRepository.getRelationship(senderId, recipientId, RelationshipTypes.FRIEND),
|
||||
]);
|
||||
const areFriends = friendship != null;
|
||||
const senderFilterLevel = areFriends
|
||||
? (senderSettings?.sensitiveContentFriendDmFilter ?? SensitiveMediaFilterLevel.SHOW)
|
||||
: (senderSettings?.sensitiveContentNonFriendDmFilter ?? SensitiveMediaFilterLevel.BLOCK);
|
||||
const recipientFilterLevel = areFriends
|
||||
? (recipientSettings?.sensitiveContentFriendDmFilter ?? SensitiveMediaFilterLevel.SHOW)
|
||||
: (recipientSettings?.sensitiveContentNonFriendDmFilter ?? SensitiveMediaFilterLevel.BLOCK);
|
||||
return {senderFilterLevel, recipientFilterLevel};
|
||||
}
|
||||
|
||||
private attachmentsToProcess(attachments?: Array<AttachmentRequestData>): Array<AttachmentToProcess> | undefined {
|
||||
if (!attachments) return undefined;
|
||||
const processed = attachments.filter(
|
||||
@@ -1033,7 +1000,7 @@ export class MessageSendService {
|
||||
channelId,
|
||||
data,
|
||||
});
|
||||
const dmNsfwContext = guild ? undefined : await this.buildDmNsfwContext(channel, user.id);
|
||||
const dmNsfwContext = guild ? undefined : await this.deps.persistenceService.resolveDmNsfwContext(channel, user.id);
|
||||
const messageId = forumStarter ? channelIdToMessageId(channel.id) : await this.generateMessageId(channel);
|
||||
let mentionData: SendMentionData | undefined;
|
||||
const shouldExtractMentions =
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createTestAccount, type TestAccount} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import {MessageContentService} from '@app/api/channel/services/message/MessageContentService';
|
||||
import {createChannelInvite, createFriendship, createGroupDmChannel} from '@app/api/channel/tests/ChannelTestUtils';
|
||||
import {ensureSessionStarted, sendMessage} from '@app/api/message/tests/MessageTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {createBuilder} from '@app/api/test/TestRequestBuilder';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
describe('group dm mature content', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness.shutdown();
|
||||
});
|
||||
|
||||
async function setup() {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
const other = await createTestAccount(harness);
|
||||
await createFriendship(harness, owner, member);
|
||||
await createFriendship(harness, owner, other);
|
||||
await ensureSessionStarted(harness, owner.token);
|
||||
const group = await createGroupDmChannel(harness, owner.token, [member.userId, other.userId]);
|
||||
return {owner, member, other, groupId: group.id};
|
||||
}
|
||||
|
||||
async function joinAsMinor(groupId: string, owner: TestAccount, status: number, code?: string) {
|
||||
const minor = await createTestAccount(harness, {dateOfBirth: '2012-01-01'});
|
||||
const invite = await createChannelInvite(harness, owner.token, groupId);
|
||||
await createBuilder(harness, minor.token).post(`/invites/${invite.code}`).body({}).expect(status, code).execute();
|
||||
}
|
||||
|
||||
function setNsfw(account: TestAccount, channelId: string, nsfw: boolean, status = 200, code?: string) {
|
||||
return createBuilder<ChannelResponse>(harness, account.token)
|
||||
.patch(`/channels/${channelId}`)
|
||||
.body({type: ChannelTypes.GROUP_DM, nsfw})
|
||||
.expect(status, code)
|
||||
.execute();
|
||||
}
|
||||
|
||||
it('lets the owner mark the group and allows mature media once marked', async () => {
|
||||
const s = await setup();
|
||||
const scope = vi.spyOn(MessageContentService.prototype, 'isNSFWContentAllowed');
|
||||
await sendMessage(harness, s.owner.token, s.groupId, 'before');
|
||||
expect(scope.mock.results.at(-1)?.value).toBe(false);
|
||||
const updated = await setNsfw(s.owner, s.groupId, true);
|
||||
expect(updated.nsfw).toBe(true);
|
||||
await sendMessage(harness, s.owner.token, s.groupId, 'after');
|
||||
expect(scope.mock.results.at(-1)?.value).toBe(true);
|
||||
const cleared = await setNsfw(s.owner, s.groupId, false);
|
||||
expect(cleared.nsfw).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects the toggle from a member who is not the owner', async () => {
|
||||
const s = await setup();
|
||||
await setNsfw(s.member, s.groupId, true, 403);
|
||||
});
|
||||
|
||||
it('refuses to mark a group that includes a minor', async () => {
|
||||
const s = await setup();
|
||||
await joinAsMinor(s.groupId, s.owner, 200);
|
||||
await setNsfw(s.owner, s.groupId, true, 400, APIErrorCodes.GROUP_DM_MATURE_CONTENT_INELIGIBLE);
|
||||
});
|
||||
|
||||
it('refuses to let a minor join a marked group', async () => {
|
||||
const s = await setup();
|
||||
await setNsfw(s.owner, s.groupId, true);
|
||||
await joinAsMinor(s.groupId, s.owner, 403, APIErrorCodes.NSFW_CONTENT_AGE_RESTRICTED);
|
||||
});
|
||||
});
|
||||
@@ -47,7 +47,6 @@ export const CONTENT_I18N_MESSAGES = {
|
||||
'report_flow.label.csam': 'Real or AI-generated images or videos of child sexual abuse',
|
||||
'report_flow.label.discovery_listing': 'Discovery listing',
|
||||
'report_flow.label.dislike': "It's just not for me",
|
||||
'report_flow.label.dsa': "Content that's illegal in the EU (Digital Services Act)",
|
||||
'report_flow.label.eating_disorder_promotion': 'Encouraging disordered eating',
|
||||
'report_flow.label.eating_disorder_promotion_profile': 'Their profile promotes eating disorders',
|
||||
'report_flow.label.email': 'Email address',
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_AR_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "صور أو مقاطع فيديو حقيقية أو مولدة بالذكاء الاصطناعي تُظهر اعتداءً جنسيًا على الأطفال",
|
||||
"report_flow.label.discovery_listing": "قائمة الاكتشاف",
|
||||
"report_flow.label.dislike": "هذا ببساطة لا يناسبني",
|
||||
"report_flow.label.dsa": "محتوى غير قانوني في الاتحاد الأوروبي (قانون الخدمات الرقمية)",
|
||||
"report_flow.label.eating_disorder_promotion": "تشجيع سلوكيات الأكل المضطربة",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "ملفهم الشخصي يروج لاضطرابات الأكل",
|
||||
"report_flow.label.email": "عنوان البريد الإلكتروني",
|
||||
|
||||
@@ -34,10 +34,6 @@
|
||||
"msgctxt": "report_flow.label.dislike",
|
||||
"msgid": "It's just not for me"
|
||||
},
|
||||
{
|
||||
"msgctxt": "report_flow.label.dsa",
|
||||
"msgid": "Content that's illegal in the EU (Digital Services Act)"
|
||||
},
|
||||
{
|
||||
"msgctxt": "report_flow.label.eating_disorder_promotion",
|
||||
"msgid": "Encouraging disordered eating"
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_BG_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Реални или генерирани от изкуствен интелект изображения или видеоклипове на сексуално насилие над деца",
|
||||
"report_flow.label.discovery_listing": "Списък за откриване",
|
||||
"report_flow.label.dislike": "Просто не е за мен",
|
||||
"report_flow.label.dsa": "Съдържание, което е незаконно в ЕС (Акт за цифровите услуги)",
|
||||
"report_flow.label.eating_disorder_promotion": "Насърчаване на хранителни разстройства",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Профилът им насърчава хранителни разстройства",
|
||||
"report_flow.label.email": "Имейл адрес",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_CS_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Skutečné nebo pomocí AI vytvořené obrázky či videa sexuálního zneužívání dětí",
|
||||
"report_flow.label.discovery_listing": "Záznam v seznamu objevování",
|
||||
"report_flow.label.dislike": "Prostě to není pro mě",
|
||||
"report_flow.label.dsa": "Obsah, který je v EU nelegální (akt o digitálních službách)",
|
||||
"report_flow.label.eating_disorder_promotion": "Nabádání k poruchám příjmu potravy",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Jejich profil propaguje poruchy příjmu potravy",
|
||||
"report_flow.label.email": "E-mailová adresa",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_DA_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Ægte eller AI-genererede billeder eller videoer af seksuelle overgreb mod børn",
|
||||
"report_flow.label.discovery_listing": "Opslag i Opdag",
|
||||
"report_flow.label.dislike": "Det er bare ikke noget for mig",
|
||||
"report_flow.label.dsa": "Indhold, der er ulovligt i EU (retsakten om digitale tjenester, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Opfordrer til spiseforstyrrelser",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Deres profil fremmer spiseforstyrrelser",
|
||||
"report_flow.label.email": "E-mailadresse",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_DE_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Reale oder KI-generierte Bilder oder Videos von sexuellem Kindesmissbrauch",
|
||||
"report_flow.label.discovery_listing": "Eintrag in „Entdecken“",
|
||||
"report_flow.label.dislike": "Das ist einfach nichts für mich",
|
||||
"report_flow.label.dsa": "Inhalte, die in der EU illegal sind (Gesetz über digitale Dienste)",
|
||||
"report_flow.label.eating_disorder_promotion": "Zu gestörtem Essverhalten ermutigen",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Das Profil der Person fördert Essstörungen",
|
||||
"report_flow.label.email": "E-Mail-Adresse",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_EL_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Πραγματικές ή δημιουργημένες με AI εικόνες ή βίντεο σεξουαλικής κακοποίησης παιδιών",
|
||||
"report_flow.label.discovery_listing": "Καταχώριση ανακάλυψης",
|
||||
"report_flow.label.dislike": "Απλά δεν μου ταιριάζει",
|
||||
"report_flow.label.dsa": "Περιεχόμενο που είναι παράνομο στην ΕΕ (πράξη για τις ψηφιακές υπηρεσίες, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Ενθάρρυνση διατροφικών διαταραχών",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Το προφίλ τους προωθεί διατροφικές διαταραχές",
|
||||
"report_flow.label.email": "Διεύθυνση email",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_EN_GB_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Real or AI-generated images or videos of child sexual abuse",
|
||||
"report_flow.label.discovery_listing": "Discovery listing",
|
||||
"report_flow.label.dislike": "It's just not for me",
|
||||
"report_flow.label.dsa": "Content that's illegal in the EU (Digital Services Act)",
|
||||
"report_flow.label.eating_disorder_promotion": "Encouraging disordered eating",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Their profile promotes eating disorders",
|
||||
"report_flow.label.email": "Email address",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_ES_419_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Imágenes o videos reales o generados por IA de abuso sexual infantil",
|
||||
"report_flow.label.discovery_listing": "Listado en Descubrir",
|
||||
"report_flow.label.dislike": "Simplemente no es para mí",
|
||||
"report_flow.label.dsa": "Contenido ilegal en la UE (Reglamento de Servicios Digitales)",
|
||||
"report_flow.label.eating_disorder_promotion": "Fomentar trastornos alimenticios",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Su perfil promueve trastornos alimenticios",
|
||||
"report_flow.label.email": "Dirección de correo electrónico",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_ES_ES_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Imágenes o vídeos reales o generados por IA de abuso sexual infantil",
|
||||
"report_flow.label.discovery_listing": "Ficha en Descubrir",
|
||||
"report_flow.label.dislike": "Simplemente no es para mí",
|
||||
"report_flow.label.dsa": "Contenido ilegal en la UE (Reglamento de Servicios Digitales)",
|
||||
"report_flow.label.eating_disorder_promotion": "Fomentar trastornos de la conducta alimentaria",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Su perfil promueve trastornos de la conducta alimentaria",
|
||||
"report_flow.label.email": "Dirección de correo electrónico",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_FI_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Aitoja tai tekoälyn luomia kuvia tai videoita lapsiin kohdistuvasta seksuaalisesta hyväksikäytöstä",
|
||||
"report_flow.label.discovery_listing": "Löydä-listaus",
|
||||
"report_flow.label.dislike": "Se ei vain ole minun juttuni",
|
||||
"report_flow.label.dsa": "Laiton sisältö EU:ssa (digipalvelusäädös, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Syömishäiriöihin kannustaminen",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Hänen profiilinsa edistää syömishäiriöitä",
|
||||
"report_flow.label.email": "Sähköpostiosoite",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_FR_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Images ou vidéos réelles ou générées par IA représentant des abus sexuels sur des enfants",
|
||||
"report_flow.label.discovery_listing": "Fiche dans Découverte",
|
||||
"report_flow.label.dislike": "Ce n'est pas pour moi",
|
||||
"report_flow.label.dsa": "Contenu illégal dans l'UE (règlement sur les services numériques, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Encourager les troubles du comportement alimentaire",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Son profil fait la promotion des troubles du comportement alimentaire",
|
||||
"report_flow.label.email": "Adresse e-mail",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_HE_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "תמונות או סרטונים של פגיעה מינית בילדים, אמיתיים או שנוצרו באמצעות AI",
|
||||
"report_flow.label.discovery_listing": "רישום בגילוי",
|
||||
"report_flow.label.dislike": "זה פשוט לא בשבילי",
|
||||
"report_flow.label.dsa": "תוכן בלתי חוקי באיחוד האירופי (חוק השירותים הדיגיטליים, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "עידוד הפרעות אכילה",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "הפרופיל שלהם מקדם הפרעות אכילה",
|
||||
"report_flow.label.email": "כתובת אימייל",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_HI_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "बाल यौन शोषण की असली या AI से बनाई गई तस्वीरें या वीडियो",
|
||||
"report_flow.label.discovery_listing": "डिस्कवरी लिस्टिंग",
|
||||
"report_flow.label.dislike": "यह बस मेरे लिए नहीं है",
|
||||
"report_flow.label.dsa": "यूरोपीय संघ में गैर-कानूनी सामग्री (डिजिटल सेवा अधिनियम)",
|
||||
"report_flow.label.eating_disorder_promotion": "खाने संबंधी विकार वाली आदतों को बढ़ावा देना",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "उनकी प्रोफ़ाइल खाने संबंधी विकारों को बढ़ावा देती है",
|
||||
"report_flow.label.email": "ईमेल पता",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_HR_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Stvarne ili AI-generirane slike ili videozapisi seksualnog zlostavljanja djece",
|
||||
"report_flow.label.discovery_listing": "Unos u Otkrivanju",
|
||||
"report_flow.label.dislike": "Jednostavno mi ne odgovara",
|
||||
"report_flow.label.dsa": "Sadržaj koji je nezakonit u EU-u (Akt o digitalnim uslugama)",
|
||||
"report_flow.label.eating_disorder_promotion": "Poticanje poremećaja hranjenja",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Njihov profil promiče poremećaje hranjenja",
|
||||
"report_flow.label.email": "Adresa e-pošte",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_HU_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Gyermekek szexuális bántalmazását ábrázoló valódi vagy mesterséges intelligencia által generált képek vagy videók",
|
||||
"report_flow.label.discovery_listing": "Listázás a Felfedezésben",
|
||||
"report_flow.label.dislike": "Egyszerűen nem nekem való",
|
||||
"report_flow.label.dsa": "Az EU-ban illegális tartalom (digitális szolgáltatásokról szóló rendelet, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Kóros étkezési szokásokra buzdítás",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "A profilja evészavarokat népszerűsít",
|
||||
"report_flow.label.email": "E-mail-cím",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_ID_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Gambar atau video kekerasan seksual terhadap anak, baik yang asli maupun buatan AI",
|
||||
"report_flow.label.discovery_listing": "Listing di Discovery",
|
||||
"report_flow.label.dislike": "Ini tidak cocok untuk saya",
|
||||
"report_flow.label.dsa": "Konten yang ilegal di UE (Undang-Undang Layanan Digital/DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Mendorong perilaku makan yang terganggu",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Profil mereka mempromosikan gangguan makan",
|
||||
"report_flow.label.email": "Alamat email",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_IT_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Immagini o video reali o generati da IA di abusi sessuali su minori",
|
||||
"report_flow.label.discovery_listing": "Elenco in Scopri",
|
||||
"report_flow.label.dislike": "Non fa per me",
|
||||
"report_flow.label.dsa": "Contenuti illegali nell'UE (Regolamento sui servizi digitali, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Incoraggiare disturbi alimentari",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Il suo profilo promuove disturbi alimentari",
|
||||
"report_flow.label.email": "Indirizzo e-mail",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_JA_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "児童性的虐待の画像・動画(実在のもの、またはAI生成のもの)",
|
||||
"report_flow.label.discovery_listing": "「見つける」の掲載内容",
|
||||
"report_flow.label.dislike": "自分には合わない",
|
||||
"report_flow.label.dsa": "EUで違法なコンテンツ(デジタルサービス法、DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "摂食障害的な食行動を促している",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "プロフィールが摂食障害を助長している",
|
||||
"report_flow.label.email": "メールアドレス",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_KO_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "아동 성착취물(실제 또는 AI로 생성된 이미지 또는 동영상)",
|
||||
"report_flow.label.discovery_listing": "디스커버리 목록",
|
||||
"report_flow.label.dislike": "그냥 내 취향이 아닙니다",
|
||||
"report_flow.label.dsa": "EU에서 불법인 콘텐츠(디지털 서비스법, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "섭식 장애 행동 조장",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "프로필에서 섭식 장애를 조장합니다",
|
||||
"report_flow.label.email": "이메일 주소",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_LT_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Tikri ar dirbtinio intelekto sukurti vaikų seksualinės prievartos vaizdai ar vaizdo įrašai",
|
||||
"report_flow.label.discovery_listing": "Atradimo sąrašas",
|
||||
"report_flow.label.dislike": "Tai tiesiog ne man",
|
||||
"report_flow.label.dsa": "Turinys, kuris neteisėtas ES (Skaitmeninių paslaugų aktas)",
|
||||
"report_flow.label.eating_disorder_promotion": "Sutrikusių mitybos įpročių skatinimas",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Jų profilis propaguoja valgymo sutrikimus",
|
||||
"report_flow.label.email": "El. pašto adresas",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_NL_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Echte of door AI gegenereerde beelden of video's van seksueel misbruik van kinderen",
|
||||
"report_flow.label.discovery_listing": "Vermelding bij Ontdekken",
|
||||
"report_flow.label.dislike": "Het is gewoon niets voor mij",
|
||||
"report_flow.label.dsa": "Inhoud die illegaal is in de EU (digitaledienstenverordening)",
|
||||
"report_flow.label.eating_disorder_promotion": "Gestoord eetgedrag aanmoedigen",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Hun profiel promoot eetstoornissen",
|
||||
"report_flow.label.email": "E-mailadres",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_NO_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Ekte eller AI-genererte bilder eller videoer av seksuelle overgrep mot barn",
|
||||
"report_flow.label.discovery_listing": "Oppføring i Oppdag",
|
||||
"report_flow.label.dislike": "Det er bare ikke noe for meg",
|
||||
"report_flow.label.dsa": "Innhold som er ulovlig i EU (forordningen om digitale tjenester, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Oppfordrer til spiseforstyrrelser",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Profilen deres fremmer spiseforstyrrelser",
|
||||
"report_flow.label.email": "E-postadresse",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_PL_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Prawdziwe lub wygenerowane przez AI obrazy lub filmy przedstawiające wykorzystywanie seksualne dzieci",
|
||||
"report_flow.label.discovery_listing": "Wpis w sekcji Odkrywanie",
|
||||
"report_flow.label.dislike": "To po prostu nie jest dla mnie",
|
||||
"report_flow.label.dsa": "Treści, które są nielegalne w UE (akt o usługach cyfrowych)",
|
||||
"report_flow.label.eating_disorder_promotion": "Zachęcanie do zaburzeń odżywiania",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Ich profil promuje zaburzenia odżywiania",
|
||||
"report_flow.label.email": "Adres e-mail",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_PT_BR_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Imagens ou vídeos reais ou gerados por IA de abuso sexual infantil",
|
||||
"report_flow.label.discovery_listing": "Listagem em Descobrir",
|
||||
"report_flow.label.dislike": "Não é para mim",
|
||||
"report_flow.label.dsa": "Conteúdo ilegal na UE (Regulamento dos Serviços Digitais)",
|
||||
"report_flow.label.eating_disorder_promotion": "Incentivando transtornos alimentares",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "O perfil dele(a) promove transtornos alimentares",
|
||||
"report_flow.label.email": "Endereço de e-mail",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_RO_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Imagini sau videoclipuri reale sau generate de AI cu abuz sexual asupra copiilor",
|
||||
"report_flow.label.discovery_listing": "Listarea în Discovery",
|
||||
"report_flow.label.dislike": "Pur și simplu nu este pentru mine",
|
||||
"report_flow.label.dsa": "Conținut ilegal în UE (Regulamentul privind serviciile digitale)",
|
||||
"report_flow.label.eating_disorder_promotion": "Încurajarea tulburărilor de alimentație",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Profilul său promovează tulburările de alimentație",
|
||||
"report_flow.label.email": "Adresă de e-mail",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_RU_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Реальные или созданные ИИ изображения или видео сексуального насилия над детьми",
|
||||
"report_flow.label.discovery_listing": "Запись в каталоге",
|
||||
"report_flow.label.dislike": "Мне это не подходит",
|
||||
"report_flow.label.dsa": "Незаконный в ЕС контент (Закон о цифровых услугах, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Поощрение расстройств пищевого поведения",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Их профиль пропагандирует расстройства пищевого поведения",
|
||||
"report_flow.label.email": "Адрес электронной почты",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_SV_SE_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Verkliga eller AI-genererade bilder eller videor av sexuella övergrepp mot barn",
|
||||
"report_flow.label.discovery_listing": "Listning i Upptäck",
|
||||
"report_flow.label.dislike": "Det är bara inget för mig",
|
||||
"report_flow.label.dsa": "Innehåll som är olagligt i EU (förordningen om digitala tjänster)",
|
||||
"report_flow.label.eating_disorder_promotion": "Uppmuntrar till ätstörningar",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Deras profil främjar ätstörningar",
|
||||
"report_flow.label.email": "E-postadress",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_TH_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "ภาพหรือวิดีโอการล่วงละเมิดทางเพศเด็ก ไม่ว่าจะเป็นของจริงหรือสร้างโดย AI",
|
||||
"report_flow.label.discovery_listing": "รายการใน Discovery",
|
||||
"report_flow.label.dislike": "ไม่เหมาะกับฉัน",
|
||||
"report_flow.label.dsa": "เนื้อหาที่ผิดกฎหมายในสหภาพยุโรป (กฎหมายบริการดิจิทัล หรือ DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "ส่งเสริมพฤติกรรมการกินที่ผิดปกติ",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "โปรไฟล์ของพวกเขาส่งเสริมความผิดปกติในการกิน",
|
||||
"report_flow.label.email": "ที่อยู่อีเมล",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_TR_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Çocuk cinsel istismarına ait gerçek veya yapay zeka ile üretilmiş görseller ya da videolar",
|
||||
"report_flow.label.discovery_listing": "Keşfet listesi",
|
||||
"report_flow.label.dislike": "Bana göre değil",
|
||||
"report_flow.label.dsa": "AB'de yasa dışı içerik (Dijital Hizmetler Yasası)",
|
||||
"report_flow.label.eating_disorder_promotion": "Düzensiz yeme davranışlarını teşvik etme",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Profili yeme bozukluklarını teşvik ediyor",
|
||||
"report_flow.label.email": "E-posta adresi",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_UK_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Реальні або згенеровані ШІ зображення чи відео сексуального насильства над дітьми",
|
||||
"report_flow.label.discovery_listing": "Запис у Discovery",
|
||||
"report_flow.label.dislike": "Мені це просто не підходить",
|
||||
"report_flow.label.dsa": "Вміст, незаконний в ЄС (Акт про цифрові послуги)",
|
||||
"report_flow.label.eating_disorder_promotion": "Заохочення розладів харчової поведінки",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Їхній профіль пропагує розлади харчової поведінки",
|
||||
"report_flow.label.email": "Адреса електронної пошти",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_VI_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "Ảnh hoặc video lạm dụng tình dục trẻ em, dù là thật hay do AI tạo ra",
|
||||
"report_flow.label.discovery_listing": "Danh sách khám phá",
|
||||
"report_flow.label.dislike": "Không hợp với tôi",
|
||||
"report_flow.label.dsa": "Nội dung bất hợp pháp tại EU (Đạo luật Dịch vụ Kỹ thuật số)",
|
||||
"report_flow.label.eating_disorder_promotion": "Khuyến khích chứng rối loạn ăn uống",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Hồ sơ của họ quảng bá chứng rối loạn ăn uống",
|
||||
"report_flow.label.email": "Địa chỉ email",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_ZH_CN_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "真实或 AI 生成的儿童性虐待图像或视频",
|
||||
"report_flow.label.discovery_listing": "社区发现列表",
|
||||
"report_flow.label.dislike": "我就是不喜欢",
|
||||
"report_flow.label.dsa": "在欧盟属违法的内容(《数字服务法》)",
|
||||
"report_flow.label.eating_disorder_promotion": "鼓励进食障碍行为",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "其个人资料宣传进食障碍",
|
||||
"report_flow.label.email": "电子邮件地址",
|
||||
|
||||
@@ -45,7 +45,6 @@ const CONTENT_I18N_ZH_TW_MESSAGES = defineContentI18nLocaleMessages({
|
||||
"report_flow.label.csam": "兒童性虐待的真實或 AI 生成圖片或影片",
|
||||
"report_flow.label.discovery_listing": "探索列表",
|
||||
"report_flow.label.dislike": "對我來說不太適合",
|
||||
"report_flow.label.dsa": "在歐盟屬違法的內容(《數位服務法》)",
|
||||
"report_flow.label.eating_disorder_promotion": "鼓勵飲食失調",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "他們的個人檔案宣傳飲食失調",
|
||||
"report_flow.label.email": "電子郵件地址",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "صور أو مقاطع فيديو حقيقية أو مولدة بالذكاء الاصطناعي تُظهر اعتداءً جنسيًا على الأطفال",
|
||||
"report_flow.label.discovery_listing": "قائمة الاكتشاف",
|
||||
"report_flow.label.dislike": "هذا ببساطة لا يناسبني",
|
||||
"report_flow.label.dsa": "محتوى غير قانوني في الاتحاد الأوروبي (قانون الخدمات الرقمية)",
|
||||
"report_flow.label.eating_disorder_promotion": "تشجيع سلوكيات الأكل المضطربة",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "ملفهم الشخصي يروج لاضطرابات الأكل",
|
||||
"report_flow.label.email": "عنوان البريد الإلكتروني",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Реални или генерирани от изкуствен интелект изображения или видеоклипове на сексуално насилие над деца",
|
||||
"report_flow.label.discovery_listing": "Списък за откриване",
|
||||
"report_flow.label.dislike": "Просто не е за мен",
|
||||
"report_flow.label.dsa": "Съдържание, което е незаконно в ЕС (Акт за цифровите услуги)",
|
||||
"report_flow.label.eating_disorder_promotion": "Насърчаване на хранителни разстройства",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Профилът им насърчава хранителни разстройства",
|
||||
"report_flow.label.email": "Имейл адрес",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Skutečné nebo pomocí AI vytvořené obrázky či videa sexuálního zneužívání dětí",
|
||||
"report_flow.label.discovery_listing": "Záznam v seznamu objevování",
|
||||
"report_flow.label.dislike": "Prostě to není pro mě",
|
||||
"report_flow.label.dsa": "Obsah, který je v EU nelegální (akt o digitálních službách)",
|
||||
"report_flow.label.eating_disorder_promotion": "Nabádání k poruchám příjmu potravy",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Jejich profil propaguje poruchy příjmu potravy",
|
||||
"report_flow.label.email": "E-mailová adresa",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Ægte eller AI-genererede billeder eller videoer af seksuelle overgreb mod børn",
|
||||
"report_flow.label.discovery_listing": "Opslag i Opdag",
|
||||
"report_flow.label.dislike": "Det er bare ikke noget for mig",
|
||||
"report_flow.label.dsa": "Indhold, der er ulovligt i EU (retsakten om digitale tjenester, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Opfordrer til spiseforstyrrelser",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Deres profil fremmer spiseforstyrrelser",
|
||||
"report_flow.label.email": "E-mailadresse",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Reale oder KI-generierte Bilder oder Videos von sexuellem Kindesmissbrauch",
|
||||
"report_flow.label.discovery_listing": "Eintrag in „Entdecken“",
|
||||
"report_flow.label.dislike": "Das ist einfach nichts für mich",
|
||||
"report_flow.label.dsa": "Inhalte, die in der EU illegal sind (Gesetz über digitale Dienste)",
|
||||
"report_flow.label.eating_disorder_promotion": "Zu gestörtem Essverhalten ermutigen",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Das Profil der Person fördert Essstörungen",
|
||||
"report_flow.label.email": "E-Mail-Adresse",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Πραγματικές ή δημιουργημένες με AI εικόνες ή βίντεο σεξουαλικής κακοποίησης παιδιών",
|
||||
"report_flow.label.discovery_listing": "Καταχώριση ανακάλυψης",
|
||||
"report_flow.label.dislike": "Απλά δεν μου ταιριάζει",
|
||||
"report_flow.label.dsa": "Περιεχόμενο που είναι παράνομο στην ΕΕ (πράξη για τις ψηφιακές υπηρεσίες, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Ενθάρρυνση διατροφικών διαταραχών",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Το προφίλ τους προωθεί διατροφικές διαταραχές",
|
||||
"report_flow.label.email": "Διεύθυνση email",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Real or AI-generated images or videos of child sexual abuse",
|
||||
"report_flow.label.discovery_listing": "Discovery listing",
|
||||
"report_flow.label.dislike": "It's just not for me",
|
||||
"report_flow.label.dsa": "Content that's illegal in the EU (Digital Services Act)",
|
||||
"report_flow.label.eating_disorder_promotion": "Encouraging disordered eating",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Their profile promotes eating disorders",
|
||||
"report_flow.label.email": "Email address",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Imágenes o videos reales o generados por IA de abuso sexual infantil",
|
||||
"report_flow.label.discovery_listing": "Listado en Descubrir",
|
||||
"report_flow.label.dislike": "Simplemente no es para mí",
|
||||
"report_flow.label.dsa": "Contenido ilegal en la UE (Reglamento de Servicios Digitales)",
|
||||
"report_flow.label.eating_disorder_promotion": "Fomentar trastornos alimenticios",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Su perfil promueve trastornos alimenticios",
|
||||
"report_flow.label.email": "Dirección de correo electrónico",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Imágenes o vídeos reales o generados por IA de abuso sexual infantil",
|
||||
"report_flow.label.discovery_listing": "Ficha en Descubrir",
|
||||
"report_flow.label.dislike": "Simplemente no es para mí",
|
||||
"report_flow.label.dsa": "Contenido ilegal en la UE (Reglamento de Servicios Digitales)",
|
||||
"report_flow.label.eating_disorder_promotion": "Fomentar trastornos de la conducta alimentaria",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Su perfil promueve trastornos de la conducta alimentaria",
|
||||
"report_flow.label.email": "Dirección de correo electrónico",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Aitoja tai tekoälyn luomia kuvia tai videoita lapsiin kohdistuvasta seksuaalisesta hyväksikäytöstä",
|
||||
"report_flow.label.discovery_listing": "Löydä-listaus",
|
||||
"report_flow.label.dislike": "Se ei vain ole minun juttuni",
|
||||
"report_flow.label.dsa": "Laiton sisältö EU:ssa (digipalvelusäädös, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Syömishäiriöihin kannustaminen",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Hänen profiilinsa edistää syömishäiriöitä",
|
||||
"report_flow.label.email": "Sähköpostiosoite",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Images ou vidéos réelles ou générées par IA représentant des abus sexuels sur des enfants",
|
||||
"report_flow.label.discovery_listing": "Fiche dans Découverte",
|
||||
"report_flow.label.dislike": "Ce n'est pas pour moi",
|
||||
"report_flow.label.dsa": "Contenu illégal dans l'UE (règlement sur les services numériques, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Encourager les troubles du comportement alimentaire",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Son profil fait la promotion des troubles du comportement alimentaire",
|
||||
"report_flow.label.email": "Adresse e-mail",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "תמונות או סרטונים של פגיעה מינית בילדים, אמיתיים או שנוצרו באמצעות AI",
|
||||
"report_flow.label.discovery_listing": "רישום בגילוי",
|
||||
"report_flow.label.dislike": "זה פשוט לא בשבילי",
|
||||
"report_flow.label.dsa": "תוכן בלתי חוקי באיחוד האירופי (חוק השירותים הדיגיטליים, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "עידוד הפרעות אכילה",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "הפרופיל שלהם מקדם הפרעות אכילה",
|
||||
"report_flow.label.email": "כתובת אימייל",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "बाल यौन शोषण की असली या AI से बनाई गई तस्वीरें या वीडियो",
|
||||
"report_flow.label.discovery_listing": "डिस्कवरी लिस्टिंग",
|
||||
"report_flow.label.dislike": "यह बस मेरे लिए नहीं है",
|
||||
"report_flow.label.dsa": "यूरोपीय संघ में गैर-कानूनी सामग्री (डिजिटल सेवा अधिनियम)",
|
||||
"report_flow.label.eating_disorder_promotion": "खाने संबंधी विकार वाली आदतों को बढ़ावा देना",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "उनकी प्रोफ़ाइल खाने संबंधी विकारों को बढ़ावा देती है",
|
||||
"report_flow.label.email": "ईमेल पता",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Stvarne ili AI-generirane slike ili videozapisi seksualnog zlostavljanja djece",
|
||||
"report_flow.label.discovery_listing": "Unos u Otkrivanju",
|
||||
"report_flow.label.dislike": "Jednostavno mi ne odgovara",
|
||||
"report_flow.label.dsa": "Sadržaj koji je nezakonit u EU-u (Akt o digitalnim uslugama)",
|
||||
"report_flow.label.eating_disorder_promotion": "Poticanje poremećaja hranjenja",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Njihov profil promiče poremećaje hranjenja",
|
||||
"report_flow.label.email": "Adresa e-pošte",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Gyermekek szexuális bántalmazását ábrázoló valódi vagy mesterséges intelligencia által generált képek vagy videók",
|
||||
"report_flow.label.discovery_listing": "Listázás a Felfedezésben",
|
||||
"report_flow.label.dislike": "Egyszerűen nem nekem való",
|
||||
"report_flow.label.dsa": "Az EU-ban illegális tartalom (digitális szolgáltatásokról szóló rendelet, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Kóros étkezési szokásokra buzdítás",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "A profilja evészavarokat népszerűsít",
|
||||
"report_flow.label.email": "E-mail-cím",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Gambar atau video kekerasan seksual terhadap anak, baik yang asli maupun buatan AI",
|
||||
"report_flow.label.discovery_listing": "Listing di Discovery",
|
||||
"report_flow.label.dislike": "Ini tidak cocok untuk saya",
|
||||
"report_flow.label.dsa": "Konten yang ilegal di UE (Undang-Undang Layanan Digital/DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Mendorong perilaku makan yang terganggu",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Profil mereka mempromosikan gangguan makan",
|
||||
"report_flow.label.email": "Alamat email",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Immagini o video reali o generati da IA di abusi sessuali su minori",
|
||||
"report_flow.label.discovery_listing": "Elenco in Scopri",
|
||||
"report_flow.label.dislike": "Non fa per me",
|
||||
"report_flow.label.dsa": "Contenuti illegali nell'UE (Regolamento sui servizi digitali, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Incoraggiare disturbi alimentari",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Il suo profilo promuove disturbi alimentari",
|
||||
"report_flow.label.email": "Indirizzo e-mail",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "児童性的虐待の画像・動画(実在のもの、またはAI生成のもの)",
|
||||
"report_flow.label.discovery_listing": "「見つける」の掲載内容",
|
||||
"report_flow.label.dislike": "自分には合わない",
|
||||
"report_flow.label.dsa": "EUで違法なコンテンツ(デジタルサービス法、DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "摂食障害的な食行動を促している",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "プロフィールが摂食障害を助長している",
|
||||
"report_flow.label.email": "メールアドレス",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "아동 성착취물(실제 또는 AI로 생성된 이미지 또는 동영상)",
|
||||
"report_flow.label.discovery_listing": "디스커버리 목록",
|
||||
"report_flow.label.dislike": "그냥 내 취향이 아닙니다",
|
||||
"report_flow.label.dsa": "EU에서 불법인 콘텐츠(디지털 서비스법, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "섭식 장애 행동 조장",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "프로필에서 섭식 장애를 조장합니다",
|
||||
"report_flow.label.email": "이메일 주소",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Tikri ar dirbtinio intelekto sukurti vaikų seksualinės prievartos vaizdai ar vaizdo įrašai",
|
||||
"report_flow.label.discovery_listing": "Atradimo sąrašas",
|
||||
"report_flow.label.dislike": "Tai tiesiog ne man",
|
||||
"report_flow.label.dsa": "Turinys, kuris neteisėtas ES (Skaitmeninių paslaugų aktas)",
|
||||
"report_flow.label.eating_disorder_promotion": "Sutrikusių mitybos įpročių skatinimas",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Jų profilis propaguoja valgymo sutrikimus",
|
||||
"report_flow.label.email": "El. pašto adresas",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Echte of door AI gegenereerde beelden of video's van seksueel misbruik van kinderen",
|
||||
"report_flow.label.discovery_listing": "Vermelding bij Ontdekken",
|
||||
"report_flow.label.dislike": "Het is gewoon niets voor mij",
|
||||
"report_flow.label.dsa": "Inhoud die illegaal is in de EU (digitaledienstenverordening)",
|
||||
"report_flow.label.eating_disorder_promotion": "Gestoord eetgedrag aanmoedigen",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Hun profiel promoot eetstoornissen",
|
||||
"report_flow.label.email": "E-mailadres",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Ekte eller AI-genererte bilder eller videoer av seksuelle overgrep mot barn",
|
||||
"report_flow.label.discovery_listing": "Oppføring i Oppdag",
|
||||
"report_flow.label.dislike": "Det er bare ikke noe for meg",
|
||||
"report_flow.label.dsa": "Innhold som er ulovlig i EU (forordningen om digitale tjenester, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Oppfordrer til spiseforstyrrelser",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Profilen deres fremmer spiseforstyrrelser",
|
||||
"report_flow.label.email": "E-postadresse",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Prawdziwe lub wygenerowane przez AI obrazy lub filmy przedstawiające wykorzystywanie seksualne dzieci",
|
||||
"report_flow.label.discovery_listing": "Wpis w sekcji Odkrywanie",
|
||||
"report_flow.label.dislike": "To po prostu nie jest dla mnie",
|
||||
"report_flow.label.dsa": "Treści, które są nielegalne w UE (akt o usługach cyfrowych)",
|
||||
"report_flow.label.eating_disorder_promotion": "Zachęcanie do zaburzeń odżywiania",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Ich profil promuje zaburzenia odżywiania",
|
||||
"report_flow.label.email": "Adres e-mail",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Imagens ou vídeos reais ou gerados por IA de abuso sexual infantil",
|
||||
"report_flow.label.discovery_listing": "Listagem em Descobrir",
|
||||
"report_flow.label.dislike": "Não é para mim",
|
||||
"report_flow.label.dsa": "Conteúdo ilegal na UE (Regulamento dos Serviços Digitais)",
|
||||
"report_flow.label.eating_disorder_promotion": "Incentivando transtornos alimentares",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "O perfil dele(a) promove transtornos alimentares",
|
||||
"report_flow.label.email": "Endereço de e-mail",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Imagini sau videoclipuri reale sau generate de AI cu abuz sexual asupra copiilor",
|
||||
"report_flow.label.discovery_listing": "Listarea în Discovery",
|
||||
"report_flow.label.dislike": "Pur și simplu nu este pentru mine",
|
||||
"report_flow.label.dsa": "Conținut ilegal în UE (Regulamentul privind serviciile digitale)",
|
||||
"report_flow.label.eating_disorder_promotion": "Încurajarea tulburărilor de alimentație",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Profilul său promovează tulburările de alimentație",
|
||||
"report_flow.label.email": "Adresă de e-mail",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Реальные или созданные ИИ изображения или видео сексуального насилия над детьми",
|
||||
"report_flow.label.discovery_listing": "Запись в каталоге",
|
||||
"report_flow.label.dislike": "Мне это не подходит",
|
||||
"report_flow.label.dsa": "Незаконный в ЕС контент (Закон о цифровых услугах, DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "Поощрение расстройств пищевого поведения",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Их профиль пропагандирует расстройства пищевого поведения",
|
||||
"report_flow.label.email": "Адрес электронной почты",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Verkliga eller AI-genererade bilder eller videor av sexuella övergrepp mot barn",
|
||||
"report_flow.label.discovery_listing": "Listning i Upptäck",
|
||||
"report_flow.label.dislike": "Det är bara inget för mig",
|
||||
"report_flow.label.dsa": "Innehåll som är olagligt i EU (förordningen om digitala tjänster)",
|
||||
"report_flow.label.eating_disorder_promotion": "Uppmuntrar till ätstörningar",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Deras profil främjar ätstörningar",
|
||||
"report_flow.label.email": "E-postadress",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "ภาพหรือวิดีโอการล่วงละเมิดทางเพศเด็ก ไม่ว่าจะเป็นของจริงหรือสร้างโดย AI",
|
||||
"report_flow.label.discovery_listing": "รายการใน Discovery",
|
||||
"report_flow.label.dislike": "ไม่เหมาะกับฉัน",
|
||||
"report_flow.label.dsa": "เนื้อหาที่ผิดกฎหมายในสหภาพยุโรป (กฎหมายบริการดิจิทัล หรือ DSA)",
|
||||
"report_flow.label.eating_disorder_promotion": "ส่งเสริมพฤติกรรมการกินที่ผิดปกติ",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "โปรไฟล์ของพวกเขาส่งเสริมความผิดปกติในการกิน",
|
||||
"report_flow.label.email": "ที่อยู่อีเมล",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Çocuk cinsel istismarına ait gerçek veya yapay zeka ile üretilmiş görseller ya da videolar",
|
||||
"report_flow.label.discovery_listing": "Keşfet listesi",
|
||||
"report_flow.label.dislike": "Bana göre değil",
|
||||
"report_flow.label.dsa": "AB'de yasa dışı içerik (Dijital Hizmetler Yasası)",
|
||||
"report_flow.label.eating_disorder_promotion": "Düzensiz yeme davranışlarını teşvik etme",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Profili yeme bozukluklarını teşvik ediyor",
|
||||
"report_flow.label.email": "E-posta adresi",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Реальні або згенеровані ШІ зображення чи відео сексуального насильства над дітьми",
|
||||
"report_flow.label.discovery_listing": "Запис у Discovery",
|
||||
"report_flow.label.dislike": "Мені це просто не підходить",
|
||||
"report_flow.label.dsa": "Вміст, незаконний в ЄС (Акт про цифрові послуги)",
|
||||
"report_flow.label.eating_disorder_promotion": "Заохочення розладів харчової поведінки",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Їхній профіль пропагує розлади харчової поведінки",
|
||||
"report_flow.label.email": "Адреса електронної пошти",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Ảnh hoặc video lạm dụng tình dục trẻ em, dù là thật hay do AI tạo ra",
|
||||
"report_flow.label.discovery_listing": "Danh sách khám phá",
|
||||
"report_flow.label.dislike": "Không hợp với tôi",
|
||||
"report_flow.label.dsa": "Nội dung bất hợp pháp tại EU (Đạo luật Dịch vụ Kỹ thuật số)",
|
||||
"report_flow.label.eating_disorder_promotion": "Khuyến khích chứng rối loạn ăn uống",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Hồ sơ của họ quảng bá chứng rối loạn ăn uống",
|
||||
"report_flow.label.email": "Địa chỉ email",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "真实或 AI 生成的儿童性虐待图像或视频",
|
||||
"report_flow.label.discovery_listing": "社区发现列表",
|
||||
"report_flow.label.dislike": "我就是不喜欢",
|
||||
"report_flow.label.dsa": "在欧盟属违法的内容(《数字服务法》)",
|
||||
"report_flow.label.eating_disorder_promotion": "鼓励进食障碍行为",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "其个人资料宣传进食障碍",
|
||||
"report_flow.label.email": "电子邮件地址",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "兒童性虐待的真實或 AI 生成圖片或影片",
|
||||
"report_flow.label.discovery_listing": "探索列表",
|
||||
"report_flow.label.dislike": "對我來說不太適合",
|
||||
"report_flow.label.dsa": "在歐盟屬違法的內容(《數位服務法》)",
|
||||
"report_flow.label.eating_disorder_promotion": "鼓勵飲食失調",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "他們的個人檔案宣傳飲食失調",
|
||||
"report_flow.label.email": "電子郵件地址",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"report_flow.label.csam": "Real or AI-generated images or videos of child sexual abuse",
|
||||
"report_flow.label.discovery_listing": "Discovery listing",
|
||||
"report_flow.label.dislike": "It's just not for me",
|
||||
"report_flow.label.dsa": "Content that's illegal in the EU (Digital Services Act)",
|
||||
"report_flow.label.eating_disorder_promotion": "Encouraging disordered eating",
|
||||
"report_flow.label.eating_disorder_promotion_profile": "Their profile promotes eating disorders",
|
||||
"report_flow.label.email": "Email address",
|
||||
|
||||
@@ -32324,6 +32324,10 @@
|
||||
"nicks": {
|
||||
"description": "Custom nicknames for users in this group DM",
|
||||
"anyOf": [{"$ref": "#/components/schemas/ChannelNicknameOverrides"}, {"type": "null"}]
|
||||
},
|
||||
"nsfw": {
|
||||
"description": "Whether the group DM is marked for mature content (owner only)",
|
||||
"type": ["boolean", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -39725,6 +39729,7 @@
|
||||
"MAX_GROUP_DM_RECIPIENTS",
|
||||
"MAX_GROUP_DMS",
|
||||
"GROUP_DM_RECIPIENTS_NOT_ADDABLE",
|
||||
"GROUP_DM_MATURE_CONTENT_INELIGIBLE",
|
||||
"MAX_GUILD_CHANNELS",
|
||||
"MAX_GUILD_MEMBERS",
|
||||
"MAX_GUILD_ROLES",
|
||||
@@ -40021,6 +40026,7 @@
|
||||
"You've reached the maximum of {count, plural, one {# group DM recipient} other {# group DM recipients}}.",
|
||||
"You've reached the maximum of {count, plural, one {# group DM} other {# group DMs}}.",
|
||||
"One or more selected users can't be added to this group DM.",
|
||||
"Not everyone in this group is eligible for mature content.",
|
||||
"You've reached the maximum of {count, plural, one {# community channel} other {# community channels}}.",
|
||||
"You've reached the maximum of {count, plural, one {# community member} other {# community members}}.",
|
||||
"You've reached the maximum of {count, plural, one {# community role} other {# community roles}}.",
|
||||
|
||||
@@ -121,7 +121,6 @@
|
||||
"message/private_info/threat_to_share": "item",
|
||||
"message/root_message/abuse": "screen:abuse",
|
||||
"message/root_message/dislike": "end",
|
||||
"message/root_message/dsa": "link:dsa",
|
||||
"message/root_message/private_info": "screen:private_info",
|
||||
"message/root_message/something_else": "screen:something_else_message",
|
||||
"message/root_message/spam": "submit:spam",
|
||||
@@ -215,7 +214,6 @@
|
||||
"user/profile_threat/violence_incitement": "submit:violence_incitement",
|
||||
"user/profile_threat/violent_threat": "submit:violent_threat",
|
||||
"user/root_user/abuse": "screen:profile_abuse",
|
||||
"user/root_user/dsa": "link:dsa",
|
||||
"user/root_user/hate_violence": "screen:profile_hate_violence",
|
||||
"user/root_user/impersonation": "screen:impersonation",
|
||||
"user/root_user/something_else": "screen:something_else_user",
|
||||
@@ -242,6 +240,8 @@
|
||||
"user/spam_profile/spam_profile": "submit:spam"
|
||||
},
|
||||
"retired": {
|
||||
"message/something_else_message/violence_misinfo": "screen:violence_misinfo"
|
||||
"message/root_message/dsa": "link:dsa",
|
||||
"message/something_else_message/violence_misinfo": "screen:violence_misinfo",
|
||||
"user/root_user/dsa": "link:dsa"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ const REPORT_FLOW_SURFACES: ReadonlyArray<ReportFlowSurface> = ['in_app', 'dsa']
|
||||
const REPORT_FLOW_ID_PATTERN = /^[a-z0-9_]{1,48}$/;
|
||||
const MAX_WALK_SCREENS = 10;
|
||||
const CRISIS_LINES_URL = 'https://befrienders.org';
|
||||
const COPYRIGHT_HELP_URL = 'https://fluxer.app/help/copyright';
|
||||
const HOSTED_REPORT_FLOW_INSTANCE: ReportFlowInstance = {selfHosted: false, guidelinesLinked: true};
|
||||
const REPORT_FLOW_CHECKED_INSTANCES: ReadonlyArray<ReportFlowInstance> = [
|
||||
HOSTED_REPORT_FLOW_INSTANCE,
|
||||
@@ -130,7 +131,6 @@ function isLinkAvailable(link: ReportFlowLinkId, instance: ReportFlowInstance):
|
||||
return true;
|
||||
case 'guidelines':
|
||||
return instance.guidelinesLinked;
|
||||
case 'dsa':
|
||||
case 'copyright':
|
||||
return !instance.selfHosted;
|
||||
}
|
||||
@@ -355,10 +355,9 @@ function collectDefinitionErrors(library: ReportFlowLibrary): Array<string> {
|
||||
checkKey(option.label, optionWhere);
|
||||
checkOutcome(option.outcome, optionWhere);
|
||||
const endsWithoutNotice = option.outcome.type === 'end' && option.outcome.noticeId === undefined;
|
||||
const linksToDsa =
|
||||
option.outcome.type === 'link' && (option.outcome.link === 'dsa' || option.outcome.link === 'copyright');
|
||||
if ((endsWithoutNotice || linksToDsa) && option.surface !== 'in_app') {
|
||||
errors.push(`${optionWhere}: a no-report or DSA link option must be limited to in_app`);
|
||||
const linksToCopyright = option.outcome.type === 'link' && option.outcome.link === 'copyright';
|
||||
if ((endsWithoutNotice || linksToCopyright) && option.surface !== 'in_app') {
|
||||
errors.push(`${optionWhere}: a no-report or copyright link option must be limited to in_app`);
|
||||
}
|
||||
}
|
||||
if (screen.checklist) {
|
||||
@@ -537,10 +536,8 @@ function getReportFlowInstance(guidelinesUrl: string | null): ReportFlowInstance
|
||||
|
||||
function getLinkUrl(link: ReportFlowLinkId, guidelinesUrl: string | null): string | null {
|
||||
switch (link) {
|
||||
case 'dsa':
|
||||
return `${Config.endpoints.webApp}/report`;
|
||||
case 'copyright':
|
||||
return `${Config.endpoints.webApp}/report?option=copyright_notice`;
|
||||
return COPYRIGHT_HELP_URL;
|
||||
case 'crisis_lines':
|
||||
return CRISIS_LINES_URL;
|
||||
case 'guidelines':
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {ReportFlowSurface} from '@fluxer/schema/src/domains/report/ReportFl
|
||||
|
||||
export type ReportFlowCopyKey = Extract<ContentI18nKey, `report_flow.${string}`>;
|
||||
|
||||
export type ReportFlowLinkId = 'dsa' | 'copyright' | 'crisis_lines' | 'guidelines';
|
||||
export type ReportFlowLinkId = 'copyright' | 'crisis_lines' | 'guidelines';
|
||||
|
||||
export type ReportFlowOutcomeDef =
|
||||
| {type: 'screen'; screenId: string}
|
||||
@@ -110,7 +110,6 @@ export const REPORT_FLOW_SCREENS: ReadonlyArray<ReportFlowScreenDef> = [
|
||||
outcome: {type: 'screen', screenId: 'something_else_message'},
|
||||
},
|
||||
{id: 'dislike', label: 'report_flow.label.dislike', outcome: {type: 'end'}, surface: 'in_app'},
|
||||
{id: 'dsa', label: 'report_flow.label.dsa', outcome: {type: 'link', link: 'dsa'}, surface: 'in_app'},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -521,7 +520,6 @@ export const REPORT_FLOW_SCREENS: ReadonlyArray<ReportFlowScreenDef> = [
|
||||
label: 'report_flow.label.something_else',
|
||||
outcome: {type: 'screen', screenId: 'something_else_user'},
|
||||
},
|
||||
{id: 'dsa', label: 'report_flow.label.dsa', outcome: {type: 'link', link: 'dsa'}, surface: 'in_app'},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -248,7 +248,7 @@ describe('report flow definitions', () => {
|
||||
expect(() => assertValidReportFlowLibrary(broken)).toThrow(/missing_screen is not defined/);
|
||||
});
|
||||
|
||||
test('a no-report or DSA option that is not limited to the app throws', () => {
|
||||
test('a no-report option that is not limited to the app throws', () => {
|
||||
const unlimitedDislike = editScreen(REPORT_FLOW_LIBRARY, 'root_message', (screen) => ({
|
||||
...screen,
|
||||
options: screen.options?.map((option) =>
|
||||
@@ -256,11 +256,6 @@ describe('report flow definitions', () => {
|
||||
),
|
||||
}));
|
||||
expect(() => assertValidReportFlowLibrary(unlimitedDislike)).toThrow(/limited to in_app/);
|
||||
const unlimitedDsaRow = editScreen(REPORT_FLOW_LIBRARY, 'root_user', (screen) => ({
|
||||
...screen,
|
||||
options: screen.options?.map((option) => (option.id === 'dsa' ? {...option, surface: undefined} : option)),
|
||||
}));
|
||||
expect(() => assertValidReportFlowLibrary(unlimitedDsaRow)).toThrow(/limited to in_app/);
|
||||
});
|
||||
|
||||
test('other broken definitions throw', () => {
|
||||
@@ -883,7 +878,6 @@ describe('report flow rendering', () => {
|
||||
'spam',
|
||||
'something_else',
|
||||
'dislike',
|
||||
'dsa',
|
||||
]);
|
||||
expect(root.options[5].outcome).toEqual({type: 'end', screen_id: null, reason: null, notice_id: null, url: null});
|
||||
expect(root.options[3].outcome).toEqual({
|
||||
@@ -893,7 +887,6 @@ describe('report flow rendering', () => {
|
||||
notice_id: null,
|
||||
url: null,
|
||||
});
|
||||
expect(root.options[6].outcome.url).toBe(`${config.endpoints.webApp}/report`);
|
||||
const copyright = message.screens
|
||||
.find((screen) => screen.id === 'something_else_message')!
|
||||
.options.find((option) => option.id === 'copyright');
|
||||
@@ -902,7 +895,7 @@ describe('report flow rendering', () => {
|
||||
screen_id: null,
|
||||
reason: null,
|
||||
notice_id: null,
|
||||
url: `${config.endpoints.webApp}/report?option=copyright_notice`,
|
||||
url: 'https://fluxer.app/help/copyright',
|
||||
});
|
||||
expect(root.checklist).toBeNull();
|
||||
expect(root.next_screen_id).toBeNull();
|
||||
@@ -920,6 +913,9 @@ describe('report flow rendering', () => {
|
||||
expect(sexual.options[0].id).toBe('minor_sexual');
|
||||
const user = getReportFlowResponse('user', 'in_app', 'en-US');
|
||||
expect(user.guidelines_url).toBe(`${config.endpoints.marketing}/guidelines`);
|
||||
expect(user.screens.find((screen) => screen.id === 'root_user')?.options.map((option) => option.id)).not.toContain(
|
||||
'dsa',
|
||||
);
|
||||
const intro = user.screens[0];
|
||||
expect(intro.next_screen_id).toBe('profile_parts');
|
||||
expect(intro.options_heading).toBe('Learn more');
|
||||
@@ -984,8 +980,8 @@ describe('report flow rendering', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('the in-app copyright rows open the DSA form on the copyright notice', () => {
|
||||
const url = `${getConfig().endpoints.webApp}/report?option=copyright_notice`;
|
||||
test('the in-app copyright rows open the copyright help page', () => {
|
||||
const url = 'https://fluxer.app/help/copyright';
|
||||
const copyrightUrl = (target: ReportFlowTargetType, screenId: string) =>
|
||||
getReportFlowResponse(target, 'in_app', 'en-US')
|
||||
.screens.find((screen) => screen.id === screenId)
|
||||
@@ -1031,7 +1027,7 @@ describe('report flow rendering', () => {
|
||||
).not.toMatch(dsaNames);
|
||||
});
|
||||
|
||||
test('self-hosted instances get no guidelines, DSA or copyright link rows', () => {
|
||||
test('self-hosted instances get no guidelines or copyright link rows', () => {
|
||||
const hostedUser = getReportFlowResponse('user', 'in_app', 'en-US');
|
||||
getConfig().instance.selfHosted = true;
|
||||
const user = getReportFlowResponse('user', 'in_app', 'en-US');
|
||||
@@ -1042,7 +1038,6 @@ describe('report flow rendering', () => {
|
||||
expect(intro.options_heading).toBeNull();
|
||||
expect(intro.next_screen_id).toBe('profile_parts');
|
||||
const userScreens = new Map(user.screens.map((screen) => [screen.id, screen]));
|
||||
expect(userScreens.get('root_user')?.options.map((option) => option.id)).not.toContain('dsa');
|
||||
expect(userScreens.get('something_else_user')?.options.map((option) => option.id)).not.toContain('copyright');
|
||||
const message = getReportFlowResponse('message', 'in_app', 'en-US');
|
||||
for (const screen of message.screens) {
|
||||
@@ -1051,7 +1046,6 @@ describe('report flow rendering', () => {
|
||||
}
|
||||
}
|
||||
const messageScreens = new Map(message.screens.map((screen) => [screen.id, screen]));
|
||||
expect(messageScreens.get('root_message')?.options.map((option) => option.id)).not.toContain('dsa');
|
||||
expect(messageScreens.get('something_else_message')?.options.map((option) => option.id)).not.toContain('copyright');
|
||||
const messageDsa = getReportFlowResponse('message', 'dsa', 'en-US');
|
||||
expect(
|
||||
|
||||
@@ -428,11 +428,12 @@ describe('Report flows', () => {
|
||||
.get('/reports/flows/message?surface=dsa')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
const appOnly = ['dislike', 'rude_language', 'dsa', 'copyright', 'worried_self_harm', 'worried_suicide'];
|
||||
const appOnly = ['dislike', 'rude_language', 'copyright', 'worried_self_harm', 'worried_suicide'];
|
||||
for (const id of appOnly) {
|
||||
expect(optionIds(inApp), id).toContain(id);
|
||||
expect(optionIds(dsa), id).not.toContain(id);
|
||||
}
|
||||
expect(optionIds(inApp)).not.toContain('dsa');
|
||||
expect(optionIds(inApp)).not.toContain('copyright_notice');
|
||||
expect(optionIds(dsa)).toContain('copyright_notice');
|
||||
expect(dsa.revision_hash).not.toBe(inApp.revision_hash);
|
||||
|
||||
@@ -556,6 +556,14 @@ export class MockStorageService implements IStorageService {
|
||||
return this.objects.has(this.objectId(bucket, key));
|
||||
}
|
||||
|
||||
setObjectLastModified(bucket: string, key: string, lastModified: Date): void {
|
||||
const object = this.objects.get(this.objectId(bucket, key));
|
||||
if (!object) {
|
||||
throw new Error(`No stored object ${key} in ${bucket}`);
|
||||
}
|
||||
object.lastModified = lastModified;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.objects.clear();
|
||||
this.multipartUploads.clear();
|
||||
|
||||
@@ -37,6 +37,24 @@ const ENTRY_STYLESHEET_LINK_PATTERN = /<link href="([^"]+\.css)" rel="stylesheet
|
||||
const CSS_URL_PATTERN = /url\(\s*['"]?([^'")]+)/gu;
|
||||
const DESKTOP_MODULE_ASSET_QUERY = /[?&]m=([a-z][a-z0-9_]{0,63})(?:&|$)/u;
|
||||
const RESERVED_DESKTOP_MODULE_NAMES = new Set(['assets', 'fluxer_renderer']);
|
||||
const DESKTOP_MODULE_ASSET_SOURCES = [
|
||||
{
|
||||
test: /[\\/]fonts[\\/]files[\\/]FluxerSans(JP|KR|SC|TC)[\\/][^\\/]+\.woff2$/u,
|
||||
moduleName: (match) => `fluxer_fonts_${match[1].toLowerCase()}`,
|
||||
},
|
||||
{
|
||||
test: /[\\/]@arborium[\\/]([a-z0-9-]+)[\\/]grammar_bg\.wasm$/u,
|
||||
moduleName: (match) => `fluxer_grammar_${match[1].replace(/-/gu, '_')}`,
|
||||
},
|
||||
{
|
||||
test: /[\\/]deepfilternet3[\\/](df_bg\.wasm|[^\\/]+\.tar\.gz)$/u,
|
||||
moduleName: () => 'fluxer_deepfilter',
|
||||
},
|
||||
{
|
||||
test: /[\\/]onnxruntime-web[\\/]dist[\\/][^\\/]+\.wasm$|[\\/]camera-effects[\\/]models[\\/][^\\/]+\.onnx$/u,
|
||||
moduleName: () => 'fluxer_camera_effects',
|
||||
},
|
||||
];
|
||||
|
||||
class UnusableDesktopModuleQueryError extends Error {
|
||||
constructor(resource) {
|
||||
@@ -55,6 +73,26 @@ function desktopModuleAssetName(resource) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function desktopModuleAssetRules(isProduction) {
|
||||
return DESKTOP_MODULE_ASSET_SOURCES.map((source) => ({
|
||||
test: source.test,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: (pathData) => {
|
||||
const match = source.test.exec(pathData.filename ?? '');
|
||||
if (match == null) {
|
||||
throw new UnusableDesktopModuleQueryError(pathData.filename ?? '');
|
||||
}
|
||||
const moduleName = source.moduleName(match);
|
||||
const extension = pathData.filename.endsWith('.tar.gz') ? '.tar.gz' : '[ext]';
|
||||
return isProduction
|
||||
? `assets/${moduleName}/[contenthash:16]${extension}`
|
||||
: `assets/${moduleName}/[name].[hash]${extension}`;
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveMode() {
|
||||
const modeIndex = process.argv.indexOf('--mode');
|
||||
if (modeIndex >= 0) {
|
||||
@@ -632,6 +670,7 @@ export default () => {
|
||||
filename: isProduction ? 'assets/[contenthash:16][ext]' : 'assets/[name].[hash][ext]',
|
||||
},
|
||||
},
|
||||
...(isDesktopRenderer ? desktopModuleAssetRules(isProduction) : []),
|
||||
{
|
||||
resourceQuery: DESKTOP_MODULE_ASSET_QUERY,
|
||||
type: 'asset/resource',
|
||||
|
||||
@@ -41,6 +41,7 @@ import {useRemScaleTracking} from '@app/features/theme/hooks/useRemScaleTracking
|
||||
import {useThemeCssVariables} from '@app/features/theme/hooks/useThemeCssVariables';
|
||||
import Theme from '@app/features/theme/state/Theme';
|
||||
import ThemeLibrary from '@app/features/theme/state/ThemeLibrary';
|
||||
import WindowsFont from '@app/features/theme/state/WindowsFont';
|
||||
import {useThemeStudioBroadcast} from '@app/features/theme_studio/state/ThemeStudioBroadcast';
|
||||
import ThemeStudioState from '@app/features/theme_studio/state/ThemeStudioState';
|
||||
import {SVGMasks} from '@app/features/ui/components/SVGMasks';
|
||||
@@ -174,6 +175,7 @@ export const AppWrapper = observer(({children}: AppWrapperProps) => {
|
||||
useDocumentClassToggle('reduced-motion', reducedMotion);
|
||||
useDocumentClassToggle('mobile-layout', MobileLayout.platformMobileDetected || MobileLayout.enabled);
|
||||
useDocumentClassToggle(UNFOCUSED_FULLY_INTERACTIVE_CLASS, stayInteractiveWhenUnfocused);
|
||||
useDocumentClassToggle('windows-fluxer-sans', WindowsFont.useFluxerSans);
|
||||
useDesktopAllowTransparency(isNative);
|
||||
useWindowEventListeners();
|
||||
useRemScaleTracking();
|
||||
|
||||
@@ -17,56 +17,61 @@
|
||||
--font-fallback-mono:
|
||||
'Noto Sans Mono', 'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', var(--font-fallback-sans);
|
||||
|
||||
--font-sans-primary: 'Fluxer Sans';
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
|
||||
var(--font-sans-primary), 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
|
||||
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'Segoe UI', 'Roboto', var(--font-fallback-sans), sans-serif;
|
||||
--font-mono: 'Fluxer Mono', 'Menlo', 'Monaco', 'Courier New', var(--font-fallback-mono), monospace;
|
||||
}
|
||||
|
||||
:root.platform-windows:not(.windows-fluxer-sans) {
|
||||
--font-sans-primary: 'Segoe UI Variable Text', 'Segoe UI';
|
||||
}
|
||||
|
||||
:root:lang(ja) {
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans JP', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
var(--font-sans-primary), 'Fluxer Sans JP', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
'Fluxer Sans Thai Looped', 'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans KR', system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', 'Meiryo',
|
||||
var(--font-fallback-sans), sans-serif;
|
||||
}
|
||||
:root:lang(ko) {
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans KR', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
var(--font-sans-primary), 'Fluxer Sans KR', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
'Fluxer Sans Thai Looped', 'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'Malgun Gothic', 'Apple SD Gothic Neo', var(--font-fallback-sans), sans-serif;
|
||||
}
|
||||
:root:lang(zh-CN),
|
||||
:root:lang(zh-Hans) {
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans SC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
var(--font-sans-primary), 'Fluxer Sans SC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
'Fluxer Sans Thai Looped', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', var(--font-fallback-sans), sans-serif;
|
||||
}
|
||||
:root:lang(zh-Hant-TW),
|
||||
:root:lang(zh-TW) {
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans TC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
var(--font-sans-primary), 'Fluxer Sans TC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
'Fluxer Sans Thai Looped', 'Fluxer Sans SC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'PingFang TC', 'Microsoft JhengHei', var(--font-fallback-sans), sans-serif;
|
||||
}
|
||||
:root:lang(zh-HK),
|
||||
:root:lang(zh-Hant-HK) {
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans TC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
var(--font-sans-primary), 'Fluxer Sans TC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
'Fluxer Sans Thai Looped', 'Fluxer Sans SC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'PingFang HK', 'Microsoft JhengHei', var(--font-fallback-sans), sans-serif;
|
||||
}
|
||||
:root:lang(ar) {
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
|
||||
var(--font-sans-primary), 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
|
||||
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'Segoe UI', 'Geeza Pro', 'Arabic Typesetting', var(--font-fallback-sans), sans-serif;
|
||||
}
|
||||
:root:lang(he) {
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans Hebrew', 'Fluxer Sans Arabic', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
|
||||
var(--font-sans-primary), 'Fluxer Sans Hebrew', 'Fluxer Sans Arabic', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
|
||||
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'Segoe UI', 'Arial Hebrew', var(--font-fallback-sans), sans-serif;
|
||||
}
|
||||
@@ -75,13 +80,13 @@
|
||||
:root:lang(ne),
|
||||
:root:lang(sa) {
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans Devanagari', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Thai Looped',
|
||||
var(--font-sans-primary), 'Fluxer Sans Devanagari', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Thai Looped',
|
||||
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'Nirmala UI', 'Noto Sans Devanagari', var(--font-fallback-sans), sans-serif;
|
||||
}
|
||||
:root:lang(th) {
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans Thai Looped', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
var(--font-sans-primary), 'Fluxer Sans Thai Looped', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
|
||||
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
|
||||
BlinkMacSystemFont, 'Leelawadee UI', 'Thonburi', var(--font-fallback-sans), sans-serif;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
getSectionIdsForTab,
|
||||
getSectionsForTab,
|
||||
getUserSettingsTabLabel,
|
||||
PRIVACY_REVIEW_NEEDED_DESCRIPTOR,
|
||||
tabHasMultipleLinkableSections,
|
||||
USER_SETTINGS_LABEL_DESCRIPTOR,
|
||||
} from '@app/features/user/components/settings_utils/SettingsConstants';
|
||||
@@ -79,6 +80,7 @@ import {buildUserSettingsDeepLink} from '@app/features/user/components/settings_
|
||||
import {useSettingsContentKey} from '@app/features/user/hooks/useSettingsContentKey';
|
||||
import {useUnsavedChangesFlash} from '@app/features/user/hooks/useUnsavedChangesFlash';
|
||||
import {ScrollSpyProvider, useScrollSpyContext} from '@app/features/user/state/ScrollSpyContext';
|
||||
import UserSettings from '@app/features/user/state/UserSettings';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import {
|
||||
collapseSettingsTreeTab,
|
||||
@@ -380,6 +382,7 @@ export const DesktopSettingsView: React.FC<DesktopSettingsViewProps> = observer(
|
||||
const activeTabPanelId = selectedTab ? `settings-tabpanel-${selectedTab}` : undefined;
|
||||
const activeTabId = selectedTab ? `settings-tab-${selectedTab}` : undefined;
|
||||
const shouldShowWhatsNew = hasWhatsNewEntries();
|
||||
const privacySetupPending = UserSettings.isPrivacySetupPending();
|
||||
const scrollKey = useMemo(() => {
|
||||
const subtabKey = contentKey ?? initialSubtab ?? 'root';
|
||||
if (isSearchActive) {
|
||||
@@ -585,6 +588,11 @@ export const DesktopSettingsView: React.FC<DesktopSettingsViewProps> = observer(
|
||||
</div>
|
||||
}
|
||||
selected={isSelected}
|
||||
attentionLabel={
|
||||
tab.type === 'privacy_safety' && privacySetupPending
|
||||
? i18n._(PRIVACY_REVIEW_NEEDED_DESCRIPTOR)
|
||||
: undefined
|
||||
}
|
||||
onClick={() => handleTabSelect(tab.type)}
|
||||
onRequestContentFocus={focusContentPanel}
|
||||
id={tabId}
|
||||
|
||||
@@ -21,6 +21,7 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import * as TextCopyCommands from '@app/features/ui/commands/TextCopyCommands';
|
||||
import {MentionBadgeAnimated} from '@app/features/ui/components/MentionBadge';
|
||||
import {NotificationDot} from '@app/features/ui/components/NotificationDot';
|
||||
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
|
||||
import {Spinner} from '@app/features/ui/components/Spinner';
|
||||
import {StatusAwareAvatar} from '@app/features/ui/components/StatusAwareAvatar';
|
||||
@@ -31,6 +32,7 @@ import type {SettingsTab} from '@app/features/user/components/settings_utils/Set
|
||||
import {
|
||||
getCategoryLabel,
|
||||
getUserSettingsTabLabel,
|
||||
PRIVACY_REVIEW_NEEDED_DESCRIPTOR,
|
||||
} from '@app/features/user/components/settings_utils/SettingsConstants';
|
||||
import {isSettingsItemNew} from '@app/features/user/components/settings_utils/SettingsMetadata';
|
||||
import {
|
||||
@@ -44,6 +46,7 @@ import {buildUserSettingsDeepLink} from '@app/features/user/components/settings_
|
||||
import type {MobileNavigationState} from '@app/features/user/hooks/useMobileNavigation';
|
||||
import {useSettingsContentKey} from '@app/features/user/hooks/useSettingsContentKey';
|
||||
import {useUnsavedChangesFlash} from '@app/features/user/hooks/useUnsavedChangesFlash';
|
||||
import UserSettings from '@app/features/user/state/UserSettings';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
@@ -137,9 +140,10 @@ interface PressableSettingsItemProps {
|
||||
tab: SettingsTab;
|
||||
onSelect: () => void;
|
||||
badge?: React.ReactNode;
|
||||
attention?: string;
|
||||
}
|
||||
|
||||
const PressableSettingsItem: React.FC<PressableSettingsItemProps> = observer(({tab, onSelect, badge}) => {
|
||||
const PressableSettingsItem: React.FC<PressableSettingsItemProps> = observer(({tab, onSelect, badge, attention}) => {
|
||||
const {isPressed, pressableProps} = usePressable();
|
||||
const {handleLongPressCopy, consumeSuppressedClick} = useSettingsPageDeepLinkLongPress(tab.type);
|
||||
const handleClick = useCallback(
|
||||
@@ -187,6 +191,9 @@ const PressableSettingsItem: React.FC<PressableSettingsItemProps> = observer(({t
|
||||
{badge}
|
||||
</div>
|
||||
</div>
|
||||
{attention && (
|
||||
<NotificationDot label={attention} data-flx="app.mobile-settings-view.pressable-settings-item.attention-dot" />
|
||||
)}
|
||||
<ArrowLeftIcon
|
||||
className={styles.settingsItemArrow}
|
||||
data-flx="app.mobile-settings-view.pressable-settings-item.settings-item-arrow"
|
||||
@@ -362,6 +369,7 @@ const MobileSettingsList = observer(
|
||||
const {i18n} = useLingui();
|
||||
const currentUser = Users.currentUser;
|
||||
const shouldShowWhatsNew = hasWhatsNewEntries();
|
||||
const privacySetupPending = UserSettings.isPrivacySetupPending();
|
||||
const advancedSettingsHasNew = useMemo(
|
||||
() =>
|
||||
getAdvancedSettingItems().some((item) => isSettingsItemNew(item, Date.now(), currentUser?.createdAt ?? null)),
|
||||
@@ -432,6 +440,11 @@ const MobileSettingsList = observer(
|
||||
tab={tab}
|
||||
onSelect={() => onTabSelect(tab.type, tab.label)}
|
||||
badge={badge}
|
||||
attention={
|
||||
tab.type === 'privacy_safety' && privacySetupPending
|
||||
? i18n._(PRIVACY_REVIEW_NEEDED_DESCRIPTOR)
|
||||
: undefined
|
||||
}
|
||||
data-flx="app.mobile-settings-view.mobile-settings-list.pressable-settings-item.tab-select"
|
||||
/>
|
||||
{(!isLastTab || isLastCategory) && (
|
||||
|
||||
+22
-1
@@ -406,9 +406,30 @@
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.sidebarItemTrailing {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-inline-start: 0.25rem;
|
||||
min-width: 1rem;
|
||||
min-height: 1rem;
|
||||
}
|
||||
|
||||
.sidebarItemAttentionDot {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.sidebarItem[data-expandable='true'].sidebarItemSelected .sidebarItemAttentionDot,
|
||||
.sidebarItem[data-expandable='true'][aria-expanded='true'] .sidebarItemAttentionDot,
|
||||
.sidebarItem[data-expandable='true']:hover .sidebarItemAttentionDot,
|
||||
.sidebarItem[data-expandable='true'].sidebarItemFocused .sidebarItemAttentionDot {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.sidebarItemChevron {
|
||||
flex-shrink: 0;
|
||||
margin-inline-start: 0.25rem;
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0;
|
||||
transform: rotate(0deg);
|
||||
|
||||
@@ -4,6 +4,7 @@ import Accessibility from '@app/features/accessibility/state/Accessibility';
|
||||
import styles from '@app/features/app/components/dialogs/shared/SettingsModalLayout.module.css';
|
||||
import {SETTINGS_SECTIONS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
import {NotificationDot} from '@app/features/ui/components/NotificationDot';
|
||||
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
|
||||
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
||||
import {
|
||||
@@ -292,6 +293,7 @@ export const SettingsModalSidebarItem: React.FC<SettingsModalSidebarItemProps> =
|
||||
expandableId,
|
||||
sectionsGroupId,
|
||||
toggleOnSelectedClick = true,
|
||||
attentionLabel,
|
||||
}) => {
|
||||
const {tabIndex, buttonRef} = useSettingsModalSidebarItemLogic({selected});
|
||||
const focusContentAfterActivationRef = useRef(false);
|
||||
@@ -354,14 +356,28 @@ export const SettingsModalSidebarItem: React.FC<SettingsModalSidebarItemProps> =
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{expandable && (
|
||||
<CaretRightIcon
|
||||
className={styles.sidebarItemChevron}
|
||||
size={remFromPx(16)}
|
||||
weight="bold"
|
||||
aria-hidden="true"
|
||||
data-flx="app.settings-modal-layout.settings-modal-sidebar-item.sidebar-item-chevron"
|
||||
/>
|
||||
{(expandable || attentionLabel) && (
|
||||
<span
|
||||
className={styles.sidebarItemTrailing}
|
||||
data-flx="app.settings-modal-layout.settings-modal-sidebar-item.sidebar-item-trailing"
|
||||
>
|
||||
{attentionLabel && (
|
||||
<NotificationDot
|
||||
className={styles.sidebarItemAttentionDot}
|
||||
label={attentionLabel}
|
||||
data-flx="app.settings-modal-layout.settings-modal-sidebar-item.attention-dot"
|
||||
/>
|
||||
)}
|
||||
{expandable && (
|
||||
<CaretRightIcon
|
||||
className={styles.sidebarItemChevron}
|
||||
size={remFromPx(16)}
|
||||
weight="bold"
|
||||
aria-hidden="true"
|
||||
data-flx="app.settings-modal-layout.settings-modal-sidebar-item.sidebar-item-chevron"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1283,10 +1283,10 @@ const SKELETON_NAGBAR_ROW_SHAPES: Record<NagbarType, SkeletonNagbarRowShape> = {
|
||||
[NagbarType.VISIONARY_MFA]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
|
||||
[NagbarType.VOICE_SESSION_RESTORE]: {tone: SkeletonNagbarTone.VOICE, hasActions: true},
|
||||
[NagbarType.TERMS_ACCEPTANCE]: {tone: SkeletonNagbarTone.LEGAL, hasActions: true},
|
||||
[NagbarType.PRIVACY_SETUP]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
|
||||
[NagbarType.SOFTWARE_ENCODER]: {tone: SkeletonNagbarTone.ENCODER, hasActions: true},
|
||||
[NagbarType.STREAMER_MODE]: {tone: SkeletonNagbarTone.STREAMER, hasActions: true},
|
||||
[NagbarType.DOMAIN_MOVED]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
|
||||
[NagbarType.WINDOWS_FONT]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
|
||||
};
|
||||
|
||||
const CONNECTION_SKELETON_NAGBAR_TONES: Record<ConnectionNoticeTone, SkeletonNagbarTone> = {
|
||||
|
||||
@@ -253,3 +253,13 @@
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.controlIconWithDot {
|
||||
mask-image: radial-gradient(circle at 85% 15%, transparent 0.34375rem, #000 calc(0.34375rem + 0.03125rem));
|
||||
}
|
||||
|
||||
.controlDot {
|
||||
position: absolute;
|
||||
top: 0.3125rem;
|
||||
right: 0.3125rem;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals';
|
||||
import {UserAreaPopout} from '@app/features/app/components/floating/UserAreaPopout';
|
||||
import styles from '@app/features/app/components/layout/UserArea.module.css';
|
||||
import {
|
||||
@@ -18,9 +17,8 @@ import {getPushToTalkHoldLabel} from '@app/features/input/utils/PushToTalkHint';
|
||||
import Presence from '@app/features/presence/state/Presence';
|
||||
import {SettingsContextMenu} from '@app/features/ui/action_menu/SettingsContextMenu';
|
||||
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import {FocusRingWrapper} from '@app/features/ui/components/FocusRingWrapper';
|
||||
import {NotificationDot} from '@app/features/ui/components/NotificationDot';
|
||||
import {StatusAwareAvatar} from '@app/features/ui/components/StatusAwareAvatar';
|
||||
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
||||
import {usePopout} from '@app/features/ui/hooks/usePopout';
|
||||
@@ -28,8 +26,13 @@ import {TooltipWithKeybind} from '@app/features/ui/keybind_hint/KeybindHint';
|
||||
import {Popout} from '@app/features/ui/popover/PopoverPopout';
|
||||
import MobileLayout from '@app/features/ui/state/MobileLayout';
|
||||
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
|
||||
import {USER_SETTINGS_LABEL_DESCRIPTOR} from '@app/features/user/components/settings_utils/SettingsConstants';
|
||||
import {openUserSettingsModal} from '@app/features/user/commands/UserSettingsModalCommands';
|
||||
import {
|
||||
USER_SETTINGS_LABEL_DESCRIPTOR,
|
||||
USER_SETTINGS_PRIVACY_REVIEW_NEEDED_DESCRIPTOR,
|
||||
} from '@app/features/user/components/settings_utils/SettingsConstants';
|
||||
import type {User} from '@app/features/user/models/User';
|
||||
import UserSettings from '@app/features/user/state/UserSettings';
|
||||
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
|
||||
import {VoiceConnectionStatus} from '@app/features/voice/components/VoiceConnectionStatus';
|
||||
import {VoiceAudioSettingsMenu} from '@app/features/voice/components/VoiceSettingsMenus';
|
||||
@@ -140,14 +143,10 @@ const UserAreaInner = observer(
|
||||
/>
|
||||
));
|
||||
};
|
||||
const handleSettingsClick = () => {
|
||||
ModalCommands.push(
|
||||
modal(
|
||||
() => <UserSettingsModal data-flx="app.user-area.handle-settings-click.user-settings-modal" />,
|
||||
'user-settings',
|
||||
),
|
||||
);
|
||||
};
|
||||
const privacySetupPending = UserSettings.isPrivacySetupPending();
|
||||
const settingsLabel = privacySetupPending
|
||||
? i18n._(USER_SETTINGS_PRIVACY_REVIEW_NEEDED_DESCRIPTOR)
|
||||
: i18n._(USER_SETTINGS_LABEL_DESCRIPTOR);
|
||||
const storeConnectedChannelId = MediaEngine.channelId;
|
||||
const forceShowVoiceConnection = DeveloperOptions.forceShowVoiceConnection;
|
||||
const hasVoiceConnection = !MobileLayout.enabled && (forceShowVoiceConnection || !!storeConnectedChannelId);
|
||||
@@ -420,7 +419,7 @@ const UserAreaInner = observer(
|
||||
<Tooltip
|
||||
text={() => (
|
||||
<TooltipWithKeybind
|
||||
label={i18n._(USER_SETTINGS_LABEL_DESCRIPTOR)}
|
||||
label={settingsLabel}
|
||||
action="system_toggle_settings"
|
||||
data-flx="app.user-area.user-area-inner.tooltip-with-keybind--3"
|
||||
/>
|
||||
@@ -431,9 +430,9 @@ const UserAreaInner = observer(
|
||||
<button
|
||||
ref={settingsButtonRef}
|
||||
type="button"
|
||||
aria-label={i18n._(USER_SETTINGS_LABEL_DESCRIPTOR)}
|
||||
aria-label={settingsLabel}
|
||||
className={clsx(styles.controlButton, settingsContextMenuOpen && styles.contextMenuHover)}
|
||||
onClick={handleSettingsClick}
|
||||
onClick={openUserSettingsModal}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -446,7 +445,16 @@ const UserAreaInner = observer(
|
||||
}}
|
||||
data-flx="app.user-area.user-area-inner.control-button.settings-click"
|
||||
>
|
||||
<GearIcon className={styles.controlIcon} data-flx="app.user-area.user-area-inner.control-icon--5" />
|
||||
<GearIcon
|
||||
className={clsx(styles.controlIcon, privacySetupPending && styles.controlIconWithDot)}
|
||||
data-flx="app.user-area.user-area-inner.control-icon--5"
|
||||
/>
|
||||
{privacySetupPending && (
|
||||
<NotificationDot
|
||||
className={styles.controlDot}
|
||||
data-flx="app.user-area.user-area-inner.privacy-review-dot"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</FocusRing>
|
||||
</Tooltip>
|
||||
|
||||
@@ -27,12 +27,11 @@ import {
|
||||
shouldShowPremiumFeatures,
|
||||
} from '@app/features/premium/utils/PremiumUtils';
|
||||
import StreamerMode from '@app/features/streamer_mode/state/StreamerMode';
|
||||
import WindowsFont, {WINDOWS_FONT_CHANGED_AT_MS} from '@app/features/theme/state/WindowsFont';
|
||||
import Nagbar from '@app/features/ui/state/Nagbar';
|
||||
import {hasUnavailableElectronNativeContext, isDesktop} from '@app/features/ui/utils/NativeUtils';
|
||||
import {hasUnavailableElectronNativeContext, isDesktop, isNativeWindows} from '@app/features/ui/utils/NativeUtils';
|
||||
import {isStandalonePwa} from '@app/features/ui/utils/PwaUtils';
|
||||
import {PRIVACY_SETUP_VERSION} from '@app/features/user/constants/PrivacySetupConstants';
|
||||
import StatusPage from '@app/features/user/state/StatusPage';
|
||||
import UserSettings from '@app/features/user/state/UserSettings';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import MediaEngine, {useVoiceEngineV2Model} from '@app/features/voice/engine/MediaEngineFacade';
|
||||
import {selectVoiceEngineV2AppConnectionWithFallback} from '@app/features/voice/engine/v2/VoiceEngineV2AppSelectors';
|
||||
@@ -260,6 +259,12 @@ export const useNagbarConditions = (): NagbarConditions => {
|
||||
})();
|
||||
const canShowSoftwareEncoder = SoftwareEncoderWarning.showWarning;
|
||||
const canShowStreamerMode = StreamerMode.shouldShowNagbar;
|
||||
const canShowWindowsFont =
|
||||
isNativeWindows() &&
|
||||
user != null &&
|
||||
user.createdAt.getTime() < WINDOWS_FONT_CHANGED_AT_MS &&
|
||||
!WindowsFont.useFluxerSans &&
|
||||
!WindowsFont.nagbarDismissed;
|
||||
const canShowDomainMoved = nagbarState.forceHideDomainMoved
|
||||
? false
|
||||
: nagbarState.forceDomainMoved
|
||||
@@ -287,13 +292,6 @@ export const useNagbarConditions = (): NagbarConditions => {
|
||||
(!user.privacyAgreedAt || user.privacyAgreedAt.toISOString() < PRIVACY_POLICY_LAST_UPDATED);
|
||||
return termsOutdated || privacyOutdated;
|
||||
})();
|
||||
const needsPrivacySetup = (() => {
|
||||
if (nagbarState.forceHidePrivacySetup) return false;
|
||||
if (nagbarState.forcePrivacySetup) return true;
|
||||
if (!user || !UserSettings.isHydrated()) return false;
|
||||
const privacySetupVersion = UserSettings.getPrivacySetupVersion();
|
||||
return privacySetupVersion !== null && privacySetupVersion < PRIVACY_SETUP_VERSION;
|
||||
})();
|
||||
return {
|
||||
canShowBuildEnvironment,
|
||||
canShowConnection,
|
||||
@@ -334,10 +332,10 @@ export const useNagbarConditions = (): NagbarConditions => {
|
||||
canShowVisionaryMfa,
|
||||
canShowVoiceSessionRestore,
|
||||
needsTermsAcceptance,
|
||||
needsPrivacySetup,
|
||||
canShowSoftwareEncoder,
|
||||
canShowStreamerMode,
|
||||
canShowDomainMoved,
|
||||
canShowWindowsFont,
|
||||
};
|
||||
};
|
||||
export const useActiveNagbars = (conditions: NagbarConditions): Array<NagbarState> => {
|
||||
@@ -367,12 +365,6 @@ export const useActiveNagbars = (conditions: NagbarConditions): Array<NagbarStat
|
||||
visible: conditions.needsTermsAcceptance,
|
||||
dismissible: false,
|
||||
},
|
||||
{
|
||||
type: NagbarType.PRIVACY_SETUP,
|
||||
priority: -2.75,
|
||||
visible: conditions.needsPrivacySetup,
|
||||
dismissible: false,
|
||||
},
|
||||
{
|
||||
type: NagbarType.SCHEDULED_MAINTENANCE,
|
||||
priority: -1,
|
||||
@@ -481,6 +473,12 @@ export const useActiveNagbars = (conditions: NagbarConditions): Array<NagbarStat
|
||||
visible: conditions.canShowDomainMoved,
|
||||
dismissible: true,
|
||||
},
|
||||
{
|
||||
type: NagbarType.WINDOWS_FONT,
|
||||
priority: 4.5,
|
||||
visible: conditions.canShowWindowsFont,
|
||||
dismissible: true,
|
||||
},
|
||||
];
|
||||
return selectVisibleNagbars(nagbars);
|
||||
}, [conditions]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user