From bef5e33eec4085426de46b87e449ac60d27b7b63 Mon Sep 17 00:00:00 2001 From: Hampus Date: Sat, 10 Oct 2026 15:45:17 +0200 Subject: [PATCH] feat(voice): add opt-in peer-to-peer calls and country rollouts (#3367) --- deploy/self-hosting/.env.example | 5 + deploy/self-hosting/docker-compose.yml | 1 + fluxer_admin/openapi-admin.json | 96 +++ fluxer_admin/src/api/types/instance_config.rs | 97 +++ fluxer_admin/src/routes/system_actions.rs | 250 ++++++- .../src/templates/pages/instance_config.rs | 226 +++++- fluxer_admin/tests/api_deserialization.rs | 25 + fluxer_admin/tests/htmx_acceptance.rs | 11 + fluxer_api/src/api/Config.ts | 1 + fluxer_api/src/api/Tables.ts | 10 + .../InstanceConfigAdminController.ts | 16 + fluxer_api/src/api/app/ControllerRegistry.ts | 2 + fluxer_api/src/api/channel/ChannelMappers.ts | 1 + .../channel/PrivateChannelSnapshot.test.ts | 1 + .../src/api/channel/PrivateChannelSnapshot.ts | 1 + .../channel/controllers/ChannelController.ts | 3 + .../ChannelDataRepository.test.ts | 1 + .../repositories/ThreadRepository.test.ts | 2 + .../channel/services/ChannelDataService.ts | 6 + .../channel/services/ChannelRequestService.ts | 3 + .../services/PersonalNotesChannelRepair.ts | 1 + .../channel_data/ChannelOperationsService.ts | 25 + .../message/MessageProcessingService.test.ts | 1 + .../services/thread/ThreadCreationService.ts | 1 + .../tests/ThreadCrossCuttingHelpers.test.ts | 1 + fluxer_api/src/api/config/APIConfig.ts | 1 + .../src/api/database/types/ChannelTypes.ts | 2 + .../src/api/database/types/VoiceTypes.ts | 42 ++ .../api/experiment/ExperimentController.ts | 12 +- .../src/api/experiment/ExperimentTargeting.ts | 7 +- .../tests/ExperimentController.test.ts | 201 ++++- .../channel/ChannelOperationsService.ts | 1 + .../services/data/GuildOperationsService.ts | 3 + .../tests/GuildChannelManagement.test.ts | 99 +++ .../KVThreadAutoArchiveQueueService.test.ts | 1 + .../api/instance/InstanceConfigRepository.ts | 23 + .../src/api/middleware/ServiceMiddleware.ts | 1 + .../src/api/middleware/ServiceSingletons.ts | 2 + fluxer_api/src/api/models/Channel.ts | 3 + fluxer_api/src/api/openapi/openapi.json | 405 +++++++--- .../ChannelRateLimitConfig.ts | 4 + fluxer_api/src/api/rpc/RpcService.ts | 5 + .../rpc/tests/RpcGuildVoiceStateP2p.test.ts | 56 ++ .../rpc/tests/RpcSessionGeoipCountry.test.ts | 35 + .../src/api/test/TestHarnessController.ts | 2 + .../repositories/UserChannelRepository.ts | 1 + .../api/user/services/UserChannelService.ts | 1 + .../ListEndpointUserPartialPrefetch.test.ts | 1 + .../src/api/utils/AuditSerializationUtils.ts | 1 + .../VoiceP2pConnectionReportController.ts | 74 ++ .../VoiceP2pConnectionReportRepository.ts | 15 + fluxer_api/src/api/voice/VoiceService.ts | 71 +- .../tests/VoiceP2pConnectionReports.test.ts | 229 ++++++ .../src/api/voice/tests/VoiceP2pGrant.test.ts | 172 +++++ fluxer_api/vitest.config.ts | 2 + fluxer_app/pkgs/livekit-client/VENDORING.md | 4 + fluxer_app/pkgs/livekit-client/src/index.ts | 26 +- fluxer_app/pkgs/livekit-client/src/options.ts | 2 + .../pkgs/livekit-client/src/room/Room.test.ts | 22 + .../pkgs/livekit-client/src/room/Room.ts | 2 +- .../src/features/app/constants/Endpoints.ts | 1 + .../channel/commands/ChannelCommands.ts | 1 + .../channel_tabs/ChannelOverviewTab.tsx | 27 + .../channel_overview_tab/shared.ts | 1 + .../src/features/channel/models/Channel.ts | 5 + .../components/settings/ForumOverviewTab.tsx | 1 + .../features/gateway/events/EventRouter.ts | 2 + .../transport/GatewayDispatchScheduling.ts | 1 + .../gateway/transport/GatewaySocket.ts | 31 +- .../gateway/types/GatewayVoiceTypes.ts | 1 + .../src/features/i18n/locales/ar/messages.po | 268 +++++-- .../locales/auto-i18n-reviewed-unchanged.json | 172 +++++ .../src/features/i18n/locales/bg/messages.po | 268 +++++-- .../src/features/i18n/locales/cs/messages.po | 268 +++++-- .../src/features/i18n/locales/da/messages.po | 268 +++++-- .../src/features/i18n/locales/de/messages.po | 268 +++++-- .../src/features/i18n/locales/el/messages.po | 268 +++++-- .../features/i18n/locales/en-GB/messages.po | 292 ++++++-- .../features/i18n/locales/en-US/messages.po | 267 +++++-- .../features/i18n/locales/es-419/messages.po | 268 +++++-- .../features/i18n/locales/es-ES/messages.po | 268 +++++-- .../src/features/i18n/locales/fi/messages.po | 268 +++++-- .../src/features/i18n/locales/fr/messages.po | 268 +++++-- .../src/features/i18n/locales/he/messages.po | 268 +++++-- .../src/features/i18n/locales/hi/messages.po | 268 +++++-- .../src/features/i18n/locales/hr/messages.po | 268 +++++-- .../src/features/i18n/locales/hu/messages.po | 268 +++++-- .../src/features/i18n/locales/id/messages.po | 268 +++++-- .../src/features/i18n/locales/it/messages.po | 268 +++++-- .../src/features/i18n/locales/ja/messages.po | 268 +++++-- .../src/features/i18n/locales/ko/messages.po | 268 +++++-- .../src/features/i18n/locales/lt/messages.po | 268 +++++-- .../src/features/i18n/locales/nl/messages.po | 268 +++++-- .../src/features/i18n/locales/no/messages.po | 268 +++++-- .../src/features/i18n/locales/pl/messages.po | 268 +++++-- .../features/i18n/locales/pt-BR/messages.po | 268 +++++-- .../src/features/i18n/locales/ro/messages.po | 268 +++++-- .../src/features/i18n/locales/ru/messages.po | 268 +++++-- .../features/i18n/locales/sv-SE/messages.po | 268 +++++-- .../src/features/i18n/locales/th/messages.po | 268 +++++-- .../src/features/i18n/locales/tr/messages.po | 268 +++++-- .../src/features/i18n/locales/uk/messages.po | 268 +++++-- .../src/features/i18n/locales/vi/messages.po | 268 +++++-- .../features/i18n/locales/zh-CN/messages.po | 268 +++++-- .../features/i18n/locales/zh-TW/messages.po | 268 +++++-- .../threads/state/ChannelThreads.test.ts | 1 + .../ui/action_menu/items/ChannelMenuData.tsx | 60 +- .../tabs/accessibility_tab/KeyboardTab.tsx | 13 + .../components/StreamSettingsMenuContent.tsx | 1 + .../components/VoiceE2EEIndicator.module.css | 12 + .../voice/components/VoiceE2EEIndicator.tsx | 23 + .../voice/components/VoiceSettingsMenus.tsx | 25 + .../alerts/VoiceP2pConsentModal.module.css | 16 + .../alerts/VoiceP2pConsentModal.tsx | 131 ++++ .../alerts/VoiceP2pPeerFailedModal.tsx | 77 ++ .../voice/engine/MediaEngineFacade.ts | 140 +++- .../engine/MediaEngineFacadeStateMachine.ts | 15 + .../voice/engine/VoiceChannelConnector.tsx | 97 ++- .../voice/engine/VoiceRoomEventBinder.ts | 3 +- .../engine/mesh/VoiceMeshConnectionReports.ts | 92 +++ .../voice/engine/mesh/VoiceMeshEngine.ts | 680 +++++++++++++++++ .../voice/engine/mesh/VoiceMeshPeer.ts | 422 +++++++++++ .../voice/engine/mesh/VoiceMeshPeers.ts | 42 ++ .../voice/engine/mesh/VoiceMeshRoom.ts | 30 + .../voice/engine/mesh/VoiceMeshSender.ts | 109 +++ .../voice/engine/mesh/VoiceMeshSignal.ts | 148 ++++ .../engine/mesh/VoiceMeshSignalClient.ts | 54 ++ .../VoiceEngineV2AppConnectionHostAdapter.ts | 25 +- .../VoiceEngineV2AppRemoteSpeakingAdapter.ts | 2 +- .../voice/events/VoiceServerUpdate.ts | 26 +- .../src/features/voice/events/VoiceSignal.ts | 30 + .../features/voice/state/ChannelP2pStatus.ts | 17 + .../features/voice/state/VoiceP2pConsent.ts | 37 + .../features/voice/state/VoiceP2pRollout.ts | 53 ++ .../src/features/voice/state/VoicePrompts.ts | 19 +- .../src/features/voice/utils/CallUtils.tsx | 32 + .../voice/utils/VideoQualityEntitlement.ts | 20 +- .../voice/utils/VoiceMessageDescriptors.ts | 126 ++++ fluxer_docs/astro.config.ts | 8 +- .../src/content/docs/admin-api/instance.mdx | 42 +- .../src/content/docs/gateway/commands.md | 44 +- .../src/content/docs/gateway/events.md | 115 ++- .../docs/gateway/limits-and-rate-limits.md | 8 +- .../docs/gateway/opcodes-and-close-codes.md | 41 +- .../src/content/docs/gateway/overview.md | 2 +- .../src/content/docs/http-api/calls.mdx | 2 + .../src/content/docs/http-api/channels.mdx | 4 +- .../src/content/docs/http-api/experiments.mdx | 26 +- .../docs/http-api/guild-audit-logs.mdx | 1 + .../content/docs/http-api/guild-members.mdx | 2 +- .../docs/http-api/peer-to-peer-voice.mdx | 89 +++ .../src/content/docs/http-api/threads.mdx | 2 +- .../content/docs/operator/configuration.mdx | 10 + fluxer_docs/src/content/docs/voice/index.md | 6 +- fluxer_gateway/src/call/call.erl | 54 +- fluxer_gateway/src/call/call_ringing.erl | 9 + fluxer_gateway/src/call/call_voice.erl | 183 +++-- fluxer_gateway/src/gateway/gateway_errors.erl | 20 +- .../src/gateway/gateway_handler.erl | 2 + .../src/gateway/gateway_handler_dispatch.erl | 29 +- .../gateway/gateway_handler_rate_limit.erl | 10 + fluxer_gateway/src/guild/guild.erl | 1 + fluxer_gateway/src/guild/guild_client.erl | 4 +- .../src/guild/guild_voice_handler.erl | 34 +- .../src/guild/voice/dm_voice_connect.erl | 16 +- .../src/guild/voice/dm_voice_state.erl | 15 + .../src/guild/voice/dm_voice_token.erl | 191 ++++- .../voice/guild_voice_connection_apply.erl | 7 +- .../voice/guild_voice_connection_confirm.erl | 41 +- .../voice/guild_voice_connection_join.erl | 97 ++- .../voice/guild_voice_connection_move.erl | 63 +- .../voice/guild_voice_connection_token.erl | 76 +- .../voice/guild_voice_connection_util.erl | 9 +- .../guild/voice/guild_voice_move_execute.erl | 13 +- .../src/guild/voice/guild_voice_region.erl | 61 +- .../src/guild/voice/guild_voice_server.erl | 34 + fluxer_gateway/src/guild/voice/voice_p2p.erl | 221 ++++++ .../src/guild/voice/voice_state_utils.erl | 10 + fluxer_gateway/src/session/session.erl | 7 + fluxer_gateway/src/session/session_init.erl | 3 +- .../src/session/session_lifecycle.erl | 2 + .../session/session_manager_shard_drain.erl | 8 + fluxer_gateway/src/session/session_voice.erl | 139 +++- .../src/session/session_voice_connect.erl | 30 +- .../src/session/session_voice_dispatch.erl | 43 +- fluxer_gateway/src/utils/constants.erl | 6 +- fluxer_gateway/test/call_p2p_tests.erl | 697 ++++++++++++++++++ fluxer_gateway/test/call_tests.erl | 4 +- fluxer_gateway/test/gateway_handler_tests.erl | 61 ++ .../test/guild_voice_broadcast_tests.erl | 1 + fluxer_gateway/test/guild_voice_p2p_tests.erl | 681 +++++++++++++++++ .../session_manager_shard_drain_tests.erl | 34 + fluxer_gateway/test/voice_p2p_tests.erl | 242 ++++++ packages/config/src/MasterConfig.ts | 1 + .../__tests__/EnvironmentOverrides.test.ts | 15 + .../src/config_loader/EnvironmentOverrides.ts | 1 + packages/constants/src/GatewayConstants.ts | 3 + packages/constants/src/LimitConstants.ts | 1 + .../user/preferences/v1/preferences.proto | 1 + .../schema/src/domains/admin/AdminSchemas.ts | 3 + .../admin/DomainMigrationSchemas.test.ts | 62 +- .../domains/admin/DomainMigrationSchemas.ts | 5 + .../src/domains/admin/VoiceP2pSchemas.test.ts | 205 ++++++ .../src/domains/admin/VoiceP2pSchemas.ts | 89 +++ .../domains/channel/ChannelRequestSchemas.ts | 4 + .../src/domains/channel/ChannelSchemas.ts | 5 + .../experiment/ExperimentBucket.test.ts | 49 +- .../domains/experiment/ExperimentBucket.ts | 23 + .../domains/experiment/ExperimentSchemas.ts | 6 + .../src/domains/gateway/GatewaySchemas.ts | 6 + packages/schema/src/domains/rpc/RpcSchemas.ts | 61 +- .../tests/RequestTransportSchemas.test.ts | 9 +- .../voice/VoiceP2pConnectionReportSchemas.ts | 67 ++ .../user/preferences/v1/preferences_pb.ts | 7 +- tools/dev/cassandra_target_schema.json | 79 ++ 215 files changed, 15840 insertions(+), 2693 deletions(-) create mode 100644 fluxer_api/src/api/rpc/tests/RpcGuildVoiceStateP2p.test.ts create mode 100644 fluxer_api/src/api/rpc/tests/RpcSessionGeoipCountry.test.ts create mode 100644 fluxer_api/src/api/voice/VoiceP2pConnectionReportController.ts create mode 100644 fluxer_api/src/api/voice/VoiceP2pConnectionReportRepository.ts create mode 100644 fluxer_api/src/api/voice/tests/VoiceP2pConnectionReports.test.ts create mode 100644 fluxer_api/src/api/voice/tests/VoiceP2pGrant.test.ts create mode 100644 fluxer_app/pkgs/livekit-client/src/room/Room.test.ts create mode 100644 fluxer_app/src/features/voice/components/alerts/VoiceP2pConsentModal.module.css create mode 100644 fluxer_app/src/features/voice/components/alerts/VoiceP2pConsentModal.tsx create mode 100644 fluxer_app/src/features/voice/components/alerts/VoiceP2pPeerFailedModal.tsx create mode 100644 fluxer_app/src/features/voice/engine/mesh/VoiceMeshConnectionReports.ts create mode 100644 fluxer_app/src/features/voice/engine/mesh/VoiceMeshEngine.ts create mode 100644 fluxer_app/src/features/voice/engine/mesh/VoiceMeshPeer.ts create mode 100644 fluxer_app/src/features/voice/engine/mesh/VoiceMeshPeers.ts create mode 100644 fluxer_app/src/features/voice/engine/mesh/VoiceMeshRoom.ts create mode 100644 fluxer_app/src/features/voice/engine/mesh/VoiceMeshSender.ts create mode 100644 fluxer_app/src/features/voice/engine/mesh/VoiceMeshSignal.ts create mode 100644 fluxer_app/src/features/voice/engine/mesh/VoiceMeshSignalClient.ts create mode 100644 fluxer_app/src/features/voice/events/VoiceSignal.ts create mode 100644 fluxer_app/src/features/voice/state/ChannelP2pStatus.ts create mode 100644 fluxer_app/src/features/voice/state/VoiceP2pConsent.ts create mode 100644 fluxer_app/src/features/voice/state/VoiceP2pRollout.ts create mode 100644 fluxer_docs/src/content/docs/http-api/peer-to-peer-voice.mdx create mode 100644 fluxer_gateway/src/guild/voice/voice_p2p.erl create mode 100644 fluxer_gateway/test/call_p2p_tests.erl create mode 100644 fluxer_gateway/test/guild_voice_p2p_tests.erl create mode 100644 fluxer_gateway/test/voice_p2p_tests.erl create mode 100644 packages/schema/src/domains/admin/VoiceP2pSchemas.test.ts create mode 100644 packages/schema/src/domains/admin/VoiceP2pSchemas.ts create mode 100644 packages/schema/src/domains/voice/VoiceP2pConnectionReportSchemas.ts diff --git a/deploy/self-hosting/.env.example b/deploy/self-hosting/.env.example index 3fb578a21..06cb80276 100644 --- a/deploy/self-hosting/.env.example +++ b/deploy/self-hosting/.env.example @@ -297,6 +297,11 @@ LIVEKIT_API_SECRET=CHANGE_ME #FLUXER_LIVEKIT_DEFAULT_REGION={"id":"default","name":"Default","emoji":"🌍","latitude":0,"longitude":0} #FLUXER_LIVEKIT_LOG_LEVEL=info +# STUN servers for peer-to-peer calls, which need the voice_p2p experiment turned +# on in the admin panel. STUN finds each peer's public address. The value below +# is an example. +#FLUXER_VOICE_P2P_STUN_URLS=stun:stun.example.com:3478 + FLUXER_KLIPY_API_KEY= #FLUXER_YOUTUBE_API_KEY= # Hosts the api never unfurls, comma separated. diff --git a/deploy/self-hosting/docker-compose.yml b/deploy/self-hosting/docker-compose.yml index 68d08bd74..75f391b56 100644 --- a/deploy/self-hosting/docker-compose.yml +++ b/deploy/self-hosting/docker-compose.yml @@ -104,6 +104,7 @@ x-fluxer-env: &fluxer-env FLUXER_LIVEKIT_INTERNAL_URL: ${FLUXER_LIVEKIT_INTERNAL_URL:-http://livekit:7880} FLUXER_LIVEKIT_DEFAULT_REGION: '${FLUXER_LIVEKIT_DEFAULT_REGION:-{"id":"default","name":"Default","emoji":"🌍","latitude":0,"longitude":0}}' FLUXER_LIVEKIT_URL: ${FLUXER_LIVEKIT_URL:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}:${FLUXER_PUBLIC_PORT:-443}}/livekit} + FLUXER_VOICE_P2P_STUN_URLS: ${FLUXER_VOICE_P2P_STUN_URLS:-} FLUXER_KLIPY_API_KEY: ${FLUXER_KLIPY_API_KEY:-} FLUXER_YOUTUBE_API_KEY: ${FLUXER_YOUTUBE_API_KEY:-} diff --git a/fluxer_admin/openapi-admin.json b/fluxer_admin/openapi-admin.json index 07f12cb52..8041e10e3 100644 --- a/fluxer_admin/openapi-admin.json +++ b/fluxer_admin/openapi-admin.json @@ -11268,6 +11268,7 @@ "gateway_rollout": {"$ref": "#/components/schemas/GatewayRolloutConfigResponse"}, "push_relay": {"$ref": "#/components/schemas/PushRelayConfigResponse"}, "domain_migration": {"$ref": "#/components/schemas/DomainMigrationConfigResponse"}, + "voice_p2p": {"$ref": "#/components/schemas/VoiceP2pConfigResponse"}, "captcha": {"$ref": "#/components/schemas/CaptchaConfigResponse"}, "experiment_delivery": {"$ref": "#/components/schemas/ExperimentDeliveryConfigResponse"}, "registration": { @@ -11682,6 +11683,7 @@ "gateway_rollout", "push_relay", "domain_migration", + "voice_p2p", "captcha", "experiment_delivery", "registration", @@ -11820,6 +11822,7 @@ "nullable": true, "allOf": [{"$ref": "#/components/schemas/DomainMigrationConfigUpdateRequest"}] }, + "voice_p2p": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/VoiceP2pConfigUpdateRequest"}]}, "captcha": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/CaptchaConfigUpdateRequest"}]}, "experiment_delivery": { "nullable": true, @@ -14679,6 +14682,10 @@ "nullable": true, "type": "string" }, + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" + }, "last_message_id": { "description": "The ID of the last message sent in this channel", "nullable": true, @@ -16314,11 +16321,38 @@ "max_counter": {"type": "integer", "minimum": 100, "maximum": 20000} } }, + "VoiceP2pConfigUpdateRequest": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000}, + "rollout_country_codes": {"allOf": [{"$ref": "#/components/schemas/ExperimentRolloutCountryCodesSchema"}]}, + "rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[\\x20-\\x7e]+$"}, + "included_user_ids": { + "maxItems": 1000, + "type": "array", + "items": {"type": "string", "pattern": "^\\d{1,20}$"} + }, + "included_guild_ids": { + "maxItems": 1000, + "type": "array", + "items": {"type": "string", "pattern": "^\\d{1,20}$"} + }, + "include_premium_users": {"type": "boolean"}, + "excluded_user_ids": { + "maxItems": 1000, + "type": "array", + "items": {"type": "string", "pattern": "^\\d{1,20}$"} + }, + "max_participants": {"type": "integer", "minimum": 2, "maximum": 4} + } + }, "DomainMigrationConfigUpdateRequest": { "type": "object", "properties": { "enabled": {"type": "boolean"}, "rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000}, + "rollout_country_codes": {"allOf": [{"$ref": "#/components/schemas/ExperimentRolloutCountryCodesSchema"}]}, "rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[\\x20-\\x7e]+$"}, "included_user_ids": { "maxItems": 1000, @@ -16355,6 +16389,11 @@ "voice_e2ee_scope": {"type": "string", "enum": ["guild_feature_only", "platform_wide"]} } }, + "ExperimentRolloutCountryCodesSchema": { + "maxItems": 250, + "type": "array", + "items": {"type": "string", "pattern": "^[A-Z]{2}$"} + }, "BillingLegacyPricesSchema": { "type": "object", "additionalProperties": { @@ -16470,12 +16509,68 @@ "required": ["enabled", "cost", "max_counter"], "additionalProperties": false }, + "VoiceP2pConfigResponse": { + "type": "object", + "properties": { + "enabled": {"default": false, "type": "boolean"}, + "config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991}, + "rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000}, + "rollout_country_codes": { + "default": [], + "allOf": [{"$ref": "#/components/schemas/ExperimentRolloutCountryCodesSchema"}] + }, + "rollout_salt": { + "default": "voice-p2p-v1", + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[\\x20-\\x7e]+$" + }, + "included_user_ids": { + "default": [], + "maxItems": 1000, + "type": "array", + "items": {"type": "string", "pattern": "^\\d{1,20}$"} + }, + "included_guild_ids": { + "default": [], + "maxItems": 1000, + "type": "array", + "items": {"type": "string", "pattern": "^\\d{1,20}$"} + }, + "include_premium_users": {"default": false, "type": "boolean"}, + "excluded_user_ids": { + "default": [], + "maxItems": 1000, + "type": "array", + "items": {"type": "string", "pattern": "^\\d{1,20}$"} + }, + "max_participants": {"default": 2, "type": "integer", "minimum": 2, "maximum": 4} + }, + "required": [ + "enabled", + "config_version", + "rollout_basis_points", + "rollout_country_codes", + "rollout_salt", + "included_user_ids", + "included_guild_ids", + "include_premium_users", + "excluded_user_ids", + "max_participants" + ], + "additionalProperties": false + }, "DomainMigrationConfigResponse": { "type": "object", "properties": { "enabled": {"default": false, "type": "boolean"}, "config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991}, "rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000}, + "rollout_country_codes": { + "default": [], + "allOf": [{"$ref": "#/components/schemas/ExperimentRolloutCountryCodesSchema"}] + }, "rollout_salt": { "default": "domain-migration-v1", "type": "string", @@ -16509,6 +16604,7 @@ "enabled", "config_version", "rollout_basis_points", + "rollout_country_codes", "rollout_salt", "included_user_ids", "included_guild_ids", diff --git a/fluxer_admin/src/api/types/instance_config.rs b/fluxer_admin/src/api/types/instance_config.rs index 69e1a9862..74c382462 100644 --- a/fluxer_admin/src/api/types/instance_config.rs +++ b/fluxer_admin/src/api/types/instance_config.rs @@ -27,6 +27,8 @@ pub struct InstanceConfigResponse { #[serde(default)] pub domain_migration: DomainMigrationConfigResponse, #[serde(default)] + pub voice_p2p: VoiceP2pConfigResponse, + #[serde(default)] pub captcha: CaptchaConfigResponse, #[serde(default)] pub experiment_delivery: ExperimentDeliveryConfigResponse, @@ -506,7 +508,10 @@ impl VoiceE2eeScope { } pub const EXPERIMENT_MAX_TARGETED_USERS: usize = 1_000; +pub const EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES: usize = 250; pub const DOMAIN_MIGRATION_DEFAULT_SALT: &str = "domain-migration-v1"; +pub const VOICE_P2P_DEFAULT_SALT: &str = "voice-p2p-v1"; +pub const VOICE_P2P_MAX_PARTICIPANTS_RANGE: std::ops::RangeInclusive = 2..=4; pub const CAPTCHA_COST_RANGE: std::ops::RangeInclusive = 1_000..=20_000; pub const CAPTCHA_MAX_COUNTER_RANGE: std::ops::RangeInclusive = 100..=20_000; @@ -530,6 +535,7 @@ pub struct DomainMigrationConfigResponse { pub enabled: bool, pub config_version: u64, pub rollout_basis_points: u32, + pub rollout_country_codes: Vec, pub rollout_salt: String, pub included_user_ids: Vec, pub included_guild_ids: Vec, @@ -545,6 +551,7 @@ impl Default for DomainMigrationConfigResponse { enabled: false, config_version: 0, rollout_basis_points: 0, + rollout_country_codes: Vec::new(), rollout_salt: DOMAIN_MIGRATION_DEFAULT_SALT.to_owned(), included_user_ids: Vec::new(), included_guild_ids: Vec::new(), @@ -563,6 +570,8 @@ pub struct DomainMigrationConfigUpdateRequest { #[serde(skip_serializing_if = "Option::is_none")] pub rollout_basis_points: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub rollout_country_codes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub rollout_salt: Option, #[serde(skip_serializing_if = "Option::is_none")] pub included_user_ids: Option>, @@ -578,6 +587,60 @@ pub struct DomainMigrationConfigUpdateRequest { pub standalone_forwarding: Option, } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(default)] +pub struct VoiceP2pConfigResponse { + pub enabled: bool, + pub config_version: u64, + pub rollout_basis_points: u32, + pub rollout_country_codes: Vec, + pub rollout_salt: String, + pub included_user_ids: Vec, + pub included_guild_ids: Vec, + pub include_premium_users: bool, + pub excluded_user_ids: Vec, + pub max_participants: u32, +} + +impl Default for VoiceP2pConfigResponse { + fn default() -> Self { + Self { + enabled: false, + config_version: 0, + rollout_basis_points: 0, + rollout_country_codes: Vec::new(), + rollout_salt: VOICE_P2P_DEFAULT_SALT.to_owned(), + included_user_ids: Vec::new(), + included_guild_ids: Vec::new(), + include_premium_users: false, + excluded_user_ids: Vec::new(), + max_participants: *VOICE_P2P_MAX_PARTICIPANTS_RANGE.start(), + } + } +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct VoiceP2pConfigUpdateRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rollout_basis_points: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rollout_country_codes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub rollout_salt: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub included_user_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub included_guild_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_premium_users: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub excluded_user_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_participants: Option, +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(default)] pub struct CaptchaConfigResponse { @@ -724,6 +787,8 @@ pub struct InstanceConfigUpdateRequest { #[serde(skip_serializing_if = "Option::is_none")] pub domain_migration: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub voice_p2p: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub captcha: Option, #[serde(skip_serializing_if = "Option::is_none")] pub experiment_delivery: Option, @@ -1028,17 +1093,22 @@ mod tests { .expect("admin schema"); let domain_migration = serde_json::from_value::(json!({})) .expect("default domain migration config"); + let voice_p2p = serde_json::from_value::(json!({})) + .expect("default voice p2p config"); let captcha = serde_json::from_value::(json!({})) .expect("default captcha config"); let delivery = serde_json::from_value::(json!({})) .expect("default delivery config"); let domain_migration = serde_json::to_value(domain_migration).expect("serializable domain migration config"); + let voice_p2p = serde_json::to_value(voice_p2p).expect("serializable voice p2p config"); let captcha = serde_json::to_value(captcha).expect("serializable captcha config"); let delivery = serde_json::to_value(delivery).expect("serializable delivery config"); let generated_domain_migration: generated_types::DomainMigrationConfigResponse = serde_json::from_value(domain_migration.clone()) .expect("generated domain migration config contract"); + let generated_voice_p2p: generated_types::VoiceP2pConfigResponse = + serde_json::from_value(voice_p2p.clone()).expect("generated voice p2p config contract"); let generated_captcha: generated_types::CaptchaConfigResponse = serde_json::from_value(captcha.clone()).expect("generated captcha config contract"); let generated_delivery: generated_types::ExperimentDeliveryConfigResponse = @@ -1048,6 +1118,11 @@ mod tests { .expect("serializable generated domain migration config"), domain_migration ); + assert_eq!( + serde_json::to_value(generated_voice_p2p) + .expect("serializable generated voice p2p config"), + voice_p2p + ); assert_eq!( serde_json::to_value(generated_captcha).expect("serializable generated captcha config"), captcha @@ -1059,6 +1134,7 @@ mod tests { ); for (name, value) in [ ("DomainMigrationConfigResponse", domain_migration), + ("VoiceP2pConfigResponse", voice_p2p), ("CaptchaConfigResponse", captcha), ("ExperimentDeliveryConfigResponse", delivery), ] { @@ -1093,4 +1169,25 @@ mod tests { json!({}) ); } + + #[test] + fn voice_p2p_update_preserves_empty_lists_and_omitted_fields() { + let update = VoiceP2pConfigUpdateRequest { + included_user_ids: Some(Vec::new()), + excluded_user_ids: Some(Vec::new()), + ..Default::default() + }; + let value = serde_json::to_value(update).expect("serializable update"); + serde_json::from_value::(value.clone()) + .expect("generated update contract"); + assert_eq!( + value, + json!({"included_user_ids": [], "excluded_user_ids": []}) + ); + assert_eq!( + serde_json::to_value(VoiceP2pConfigUpdateRequest::default()) + .expect("serializable update"), + json!({}) + ); + } } diff --git a/fluxer_admin/src/routes/system_actions.rs b/fluxer_admin/src/routes/system_actions.rs index a46658fdd..8275b0168 100644 --- a/fluxer_admin/src/routes/system_actions.rs +++ b/fluxer_admin/src/routes/system_actions.rs @@ -8,18 +8,19 @@ use crate::{ AppPublicConfigUpdateRequest, AppRegistrationConfigUpdateRequest, AppSetupConfigUpdateRequest, CAPTCHA_COST_RANGE, CAPTCHA_MAX_COUNTER_RANGE, CaptchaConfigUpdateRequest, CreateRegistrationUrlRequest, - DomainMigrationConfigUpdateRequest, EXPERIMENT_MAX_TARGETED_USERS, - ExperimentDeliveryConfigUpdateRequest, GatewayRolloutConfigUpdateRequest, - GatewayRolloutMode, InstanceAttachmentDecayUpdateRequest, - InstanceBlueskyIntegrationUpdateRequest, InstanceBlueskyKeyIntegrationUpdateRequest, - InstanceConfigUpdateRequest, InstanceEmailIntegrationUpdateRequest, - InstanceEmailSmtpIntegrationUpdateRequest, InstanceEmailSmtpTestRequest, - InstanceGifIntegrationUpdateRequest, InstanceIntegrationsUpdateRequest, - InstanceMediaUpdateRequest, InstancePolicyUpdateRequest, - InstanceRegistrationConfigUpdateRequest, InstanceServicesUpdateRequest, - InstanceYoutubeIntegrationUpdateRequest, LimitConfigUpdateRequest, LimitRule, - LimitRuleFilters, PremiumMode, PushRelayConfigUpdateRequest, RegistrationMode, - SsoConfigUpdateRequest, VoiceE2eeScope, + DomainMigrationConfigUpdateRequest, EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES, + EXPERIMENT_MAX_TARGETED_USERS, ExperimentDeliveryConfigUpdateRequest, + GatewayRolloutConfigUpdateRequest, GatewayRolloutMode, + InstanceAttachmentDecayUpdateRequest, InstanceBlueskyIntegrationUpdateRequest, + InstanceBlueskyKeyIntegrationUpdateRequest, InstanceConfigUpdateRequest, + InstanceEmailIntegrationUpdateRequest, InstanceEmailSmtpIntegrationUpdateRequest, + InstanceEmailSmtpTestRequest, InstanceGifIntegrationUpdateRequest, + InstanceIntegrationsUpdateRequest, InstanceMediaUpdateRequest, + InstancePolicyUpdateRequest, InstanceRegistrationConfigUpdateRequest, + InstanceServicesUpdateRequest, InstanceYoutubeIntegrationUpdateRequest, + LimitConfigUpdateRequest, LimitRule, LimitRuleFilters, PremiumMode, + PushRelayConfigUpdateRequest, RegistrationMode, SsoConfigUpdateRequest, + VOICE_P2P_MAX_PARTICIPANTS_RANGE, VoiceE2eeScope, VoiceP2pConfigUpdateRequest, }, }, config::AdminConfig, @@ -220,6 +221,10 @@ pub async fn instance_config_post( Ok(update) => instance_config_result(client.update_instance_config(&update).await), Err(message) => FlashData::error(message), }, + "update_voice_p2p" => match build_voice_p2p_update(&form) { + Ok(update) => instance_config_result(client.update_instance_config(&update).await), + Err(message) => FlashData::error(message), + }, "update_captcha" => match build_captcha_update(&form) { Ok(update) => instance_config_result(client.update_instance_config(&update).await), Err(message) => FlashData::error(message), @@ -557,6 +562,32 @@ fn parse_experiment_user_ids(value: &str, label: &str) -> Result, St Ok(ids) } +fn parse_experiment_country_codes(value: &str) -> Result, String> { + let mut codes: Vec = Vec::new(); + let candidates = value + .split(|character: char| character == ',' || character.is_whitespace()) + .filter(|candidate| !candidate.is_empty()); + for (index, candidate) in candidates.enumerate() { + if candidate.len() != 2 || !candidate.bytes().all(|byte| byte.is_ascii_alphabetic()) { + return Err(format!( + "Rollout countries entry {} must be a two-letter country code", + index + 1 + )); + } + let code = candidate.to_ascii_uppercase(); + if codes.contains(&code) { + continue; + } + if codes.len() == EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES { + return Err(format!( + "Rollout countries must contain at most {EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES} unique codes" + )); + } + codes.push(code); + } + Ok(codes) +} + fn build_push_relay_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest { InstanceConfigUpdateRequest { push_relay: Some(PushRelayConfigUpdateRequest { @@ -579,6 +610,10 @@ fn build_domain_migration_update( 0, EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX, )?, + rollout_country_codes: Some(parse_experiment_country_codes( + form.first("domain_migration_rollout_country_codes") + .unwrap_or_default(), + )?), rollout_salt: parse_experiment_rollout_salt(form, "domain_migration_rollout_salt")?, included_user_ids: Some(parse_experiment_user_ids( form.first("domain_migration_included_user_ids") @@ -609,6 +644,50 @@ fn build_domain_migration_update( }) } +fn build_voice_p2p_update(form: &MultiValueForm) -> Result { + Ok(InstanceConfigUpdateRequest { + voice_p2p: Some(VoiceP2pConfigUpdateRequest { + enabled: Some(form.bool_value("voice_p2p_enabled")), + rollout_basis_points: parse_form_number( + form, + "voice_p2p_rollout_basis_points", + "Rollout basis points", + 0, + EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX, + )?, + rollout_country_codes: Some(parse_experiment_country_codes( + form.first("voice_p2p_rollout_country_codes") + .unwrap_or_default(), + )?), + rollout_salt: parse_experiment_rollout_salt(form, "voice_p2p_rollout_salt")?, + included_user_ids: Some(parse_experiment_user_ids( + form.first("voice_p2p_included_user_ids") + .unwrap_or_default(), + "Included user IDs", + )?), + included_guild_ids: Some(parse_experiment_user_ids( + form.first("voice_p2p_included_guild_ids") + .unwrap_or_default(), + "Included guild IDs", + )?), + include_premium_users: Some(form.bool_value("voice_p2p_include_premium_users")), + excluded_user_ids: Some(parse_experiment_user_ids( + form.first("voice_p2p_excluded_user_ids") + .unwrap_or_default(), + "Excluded user IDs", + )?), + max_participants: parse_form_number( + form, + "voice_p2p_max_participants", + "Maximum participants", + *VOICE_P2P_MAX_PARTICIPANTS_RANGE.start(), + *VOICE_P2P_MAX_PARTICIPANTS_RANGE.end(), + )?, + }), + ..Default::default() + }) +} + fn build_captcha_update(form: &MultiValueForm) -> Result { Ok(InstanceConfigUpdateRequest { captcha: Some(CaptchaConfigUpdateRequest { @@ -1340,10 +1419,62 @@ mod tests { ); } + #[test] + fn parse_experiment_country_codes_splits_uppercases_and_dedupes() { + assert_eq!( + parse_experiment_country_codes(" se ,NO\nbr\r\n fi\tSE ,, no ").expect("valid codes"), + vec![ + "SE".to_owned(), + "NO".to_owned(), + "BR".to_owned(), + "FI".to_owned() + ] + ); + assert_eq!( + parse_experiment_country_codes(" \n ").expect("valid codes"), + Vec::::new() + ); + } + + #[test] + fn parse_experiment_country_codes_rejects_malformed_codes() { + for value in ["S", "SWE", "S1", "5E", "<>", "ÅL"] { + assert_eq!( + parse_experiment_country_codes(&format!("SE,{value}")).expect_err("invalid code"), + "Rollout countries entry 2 must be a two-letter country code", + "{value}" + ); + } + } + + #[test] + fn parse_experiment_country_codes_rejects_exceeding_the_cap() { + let codes = (b'A'..=b'Z') + .flat_map(|first| { + (b'A'..=b'Z').map(move |second| format!("{}{}", first as char, second as char)) + }) + .collect::>(); + let at_cap = codes[..EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES].join(" "); + assert_eq!( + parse_experiment_country_codes(&format!("{at_cap} aa")) + .expect("valid codes at cap") + .len(), + EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES + ); + assert_eq!( + parse_experiment_country_codes(&format!( + "{at_cap} {}", + codes[EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES] + )) + .expect_err("too many codes"), + "Rollout countries must contain at most 250 unique codes" + ); + } + #[test] fn build_domain_migration_update_reads_the_rollout_fields() { let form = MultiValueForm::parse( - b"domain_migration_enabled=true&domain_migration_rollout_basis_points=%20250%20&domain_migration_rollout_salt=%20domain-migration-v2%20&domain_migration_included_user_ids=1500000000000000001%0A1500000000000000002&domain_migration_excluded_user_ids=1500000000000000003%2C%201500000000000000004&domain_migration_anonymous_rollout_basis_points=%20100%20&domain_migration_standalone_forwarding=true&domain_migration_included_guild_ids=1500000000000000005%0A1500000000000000006%2C1500000000000000005&domain_migration_include_premium_users=true", + b"domain_migration_enabled=true&domain_migration_rollout_basis_points=%20250%20&domain_migration_rollout_salt=%20domain-migration-v2%20&domain_migration_included_user_ids=1500000000000000001%0A1500000000000000002&domain_migration_excluded_user_ids=1500000000000000003%2C%201500000000000000004&domain_migration_anonymous_rollout_basis_points=%20100%20&domain_migration_standalone_forwarding=true&domain_migration_included_guild_ids=1500000000000000005%0A1500000000000000006%2C1500000000000000005&domain_migration_include_premium_users=true&domain_migration_rollout_country_codes=se%2C%20NO%0Ase", ); let update = build_domain_migration_update(&form) .expect("valid form") @@ -1369,6 +1500,10 @@ mod tests { assert_eq!(update.anonymous_rollout_basis_points, Some(100)); assert_eq!(update.standalone_forwarding, Some(true)); assert_eq!(update.include_premium_users, Some(true)); + assert_eq!( + update.rollout_country_codes, + Some(vec!["SE".to_owned(), "NO".to_owned()]) + ); assert_eq!( update.included_guild_ids, Some(vec![ @@ -1386,6 +1521,7 @@ mod tests { serde_json::to_value(request).expect("serializable update"), serde_json::json!({"domain_migration": { "enabled": false, + "rollout_country_codes": [], "included_user_ids": [], "included_guild_ids": [], "include_premium_users": false, @@ -1430,6 +1566,10 @@ mod tests { "domain_migration_excluded_user_ids=123%2Cinvalid", "Excluded user IDs entry 2 must contain 1 to 20 decimal digits", ), + ( + "domain_migration_rollout_country_codes=SE%2CSWE", + "Rollout countries entry 2 must be a two-letter country code", + ), ] { let form = MultiValueForm::parse(form.as_bytes()); assert_eq!( @@ -1519,6 +1659,90 @@ mod tests { ); } + #[test] + fn build_voice_p2p_update_reads_the_rollout_fields() { + let form = MultiValueForm::parse( + b"voice_p2p_enabled=true&voice_p2p_rollout_basis_points=%20500%20&voice_p2p_rollout_salt=%20voice-p2p-v2%20&voice_p2p_included_user_ids=1500000000000000001&voice_p2p_excluded_user_ids=1500000000000000002&voice_p2p_included_guild_ids=1500000000000000005%0A1500000000000000006%2C1500000000000000005&voice_p2p_include_premium_users=true&voice_p2p_rollout_country_codes=se%2C%20NO%0Ase&voice_p2p_max_participants=%203%20", + ); + let update = build_voice_p2p_update(&form) + .expect("valid form") + .voice_p2p + .expect("voice p2p update"); + assert_eq!(update.enabled, Some(true)); + assert_eq!(update.rollout_basis_points, Some(500)); + assert_eq!(update.rollout_salt, Some("voice-p2p-v2".to_owned())); + assert_eq!(update.include_premium_users, Some(true)); + assert_eq!(update.max_participants, Some(3)); + assert_eq!( + update.rollout_country_codes, + Some(vec!["SE".to_owned(), "NO".to_owned()]) + ); + assert_eq!( + update.included_guild_ids, + Some(vec![ + "1500000000000000005".to_owned(), + "1500000000000000006".to_owned() + ]) + ); + assert_eq!( + update.included_user_ids, + Some(vec!["1500000000000000001".to_owned()]) + ); + assert_eq!( + update.excluded_user_ids, + Some(vec!["1500000000000000002".to_owned()]) + ); + } + + #[test] + fn build_voice_p2p_update_leaves_the_feature_inert_when_nothing_is_submitted() { + let form = MultiValueForm::parse(b"_csrf=token"); + let request = build_voice_p2p_update(&form).expect("valid form"); + assert_eq!( + serde_json::to_value(request).expect("serializable update"), + serde_json::json!({"voice_p2p": { + "enabled": false, + "rollout_country_codes": [], + "included_user_ids": [], + "included_guild_ids": [], + "include_premium_users": false, + "excluded_user_ids": [], + }}) + ); + } + + #[test] + fn build_voice_p2p_update_rejects_invalid_rollout_fields() { + for (form, message) in [ + ( + "voice_p2p_rollout_basis_points=10001", + "Rollout basis points must be a whole number between 0 and 10000", + ), + ( + "voice_p2p_included_guild_ids=1500000000000000005%0Anot-a-guild", + "Included guild IDs entry 2 must contain 1 to 20 decimal digits", + ), + ( + "voice_p2p_rollout_country_codes=SE%2CSWE", + "Rollout countries entry 2 must be a two-letter country code", + ), + ( + "voice_p2p_max_participants=5", + "Maximum participants must be a whole number between 2 and 4", + ), + ( + "voice_p2p_max_participants=1", + "Maximum participants must be a whole number between 2 and 4", + ), + ] { + let form = MultiValueForm::parse(form.as_bytes()); + assert_eq!( + build_voice_p2p_update(&form).expect_err("invalid field"), + message + ); + } + } + #[test] fn build_experiment_delivery_update_leaves_both_fields_unchanged_when_absent() { let form = MultiValueForm::parse(b"_csrf=token"); diff --git a/fluxer_admin/src/templates/pages/instance_config.rs b/fluxer_admin/src/templates/pages/instance_config.rs index de4930e7f..49c949d59 100644 --- a/fluxer_admin/src/templates/pages/instance_config.rs +++ b/fluxer_admin/src/templates/pages/instance_config.rs @@ -5,11 +5,12 @@ use crate::{ AccountIdentityConfigResponse, AccountIdentityMode, AppPublicConfigResponse, CAPTCHA_COST_RANGE, CAPTCHA_MAX_COUNTER_RANGE, CaptchaConfigResponse, DOMAIN_MIGRATION_DEFAULT_SALT, DomainMigrationConfigResponse, - EXPERIMENT_MAX_TARGETED_USERS, ExperimentDeliveryConfigResponse, - GatewayRolloutConfigResponse, InstanceConfigResponse, InstanceIntegrationsResponse, - InstanceMediaResponse, InstancePolicyResponse, InstanceRegistrationResponse, - LimitConfigResponse, PendingRegistrationResponse, PushRelayConfigResponse, - RegistrationUrlResponse, SsoConfigResponse, TagStyle, + EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES, EXPERIMENT_MAX_TARGETED_USERS, + ExperimentDeliveryConfigResponse, GatewayRolloutConfigResponse, InstanceConfigResponse, + InstanceIntegrationsResponse, InstanceMediaResponse, InstancePolicyResponse, + InstanceRegistrationResponse, LimitConfigResponse, PendingRegistrationResponse, + PushRelayConfigResponse, RegistrationUrlResponse, SsoConfigResponse, TagStyle, + VOICE_P2P_DEFAULT_SALT, VOICE_P2P_MAX_PARTICIPANTS_RANGE, VoiceP2pConfigResponse, }, config::AdminConfig, middleware::auth::AuthContext, @@ -58,6 +59,22 @@ fn entry_count_hint(count: usize, cap: usize) -> Markup { } } +fn experiment_rollout_country_codes_field(name: &str, country_codes: &[String]) -> Markup { + html! { + div class="flex flex-col gap-2" { + (text_input(name, "Rollout Countries", &country_codes.join(", "), "SE, NO, FI")) + (entry_count_hint(country_codes.len(), EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES)) + p class="text-xs text-neutral-500" { + "Two-letter ISO 3166-1 country codes, separated by commas or spaces. When set, \ + the user rollout percentage only applies to requests from these countries, and \ + a request with an unknown country is left out. Leave empty to apply the \ + percentage everywhere. The always-on lists, the premium switch and the \ + never-on list ignore the country. Invalid entries prevent the save." + } + } + } +} + fn number_field( name: &str, label: &str, @@ -193,6 +210,7 @@ pub fn instance_config_page( @if !instance_config.self_hosted { (domain_migration_section(base, csrf_token, &instance_config.domain_migration)) } + (voice_p2p_section(base, csrf_token, &instance_config.voice_p2p)) (experiment_delivery_section(base, csrf_token, &instance_config.experiment_delivery)) @if let Some(limit_config) = limit_config { (limit_config_section(base, limit_config)) @@ -1193,6 +1211,10 @@ fn domain_migration_section( Some(0), Some(10000), "1", Some("Share of logged-out devices sent to the new origin, in basis points. Each device is bucketed on its own random ID."), )) + (experiment_rollout_country_codes_field( + "domain_migration_rollout_country_codes", + &domain_migration.rollout_country_codes, + )) div class="flex flex-col gap-2" { (text_input( "domain_migration_rollout_salt", @@ -1287,6 +1309,162 @@ fn domain_migration_section( ) } +fn voice_p2p_section(base: &str, csrf_token: &str, voice_p2p: &VoiceP2pConfigResponse) -> Markup { + let status = if voice_p2p.enabled { + ("Live", BadgeVariant::Success) + } else { + ("Inert", BadgeVariant::Default) + }; + let included_user_ids = voice_p2p.included_user_ids.join("\n"); + let excluded_user_ids = voice_p2p.excluded_user_ids.join("\n"); + section_card_with_description( + "Peer-to-peer voice", + "Lets selected users start small voice calls that connect the participants directly \ + instead of through the voice servers. A peer-to-peer call only moves to the voice \ + servers when a participant switches it or its region changes.", + html! { + form method="post" action={(base) "/instance-config?action=update_voice_p2p"} { + (csrf_input(csrf_token)) + div class="space-y-6" { + div class="flex flex-wrap items-center gap-2" { + h3 class="text-sm font-semibold text-neutral-900" { "Master switch" } + (badge(status.0, status.1)) + span class="text-xs text-neutral-500" { + "Config version " (voice_p2p.config_version) + } + } + (checkbox( + "voice_p2p_enabled", + "true", + "Let the selected users start peer-to-peer calls", + voice_p2p.enabled, + true, + )) + p class="text-xs text-neutral-500" { + "Off is the safe state and the kill switch. With this unchecked every new \ + call uses the voice servers and a running peer-to-peer call accepts no \ + new joins, so the rollout and targeting fields below have no effect at \ + all. While it is on, the fields below select who can start a \ + peer-to-peer call, and anyone who agrees to a direct connection can \ + join one." + } + + h3 class="text-sm font-semibold text-neutral-900" { "Call size" } + (number_field( + "voice_p2p_max_participants", + "Maximum participants", + &voice_p2p.max_participants.to_string(), + Some(*VOICE_P2P_MAX_PARTICIPANTS_RANGE.start()), + Some(*VOICE_P2P_MAX_PARTICIPANTS_RANGE.end()), + "1", + Some("Most participants a peer-to-peer call can hold, 2 through 4. Clients only offer a peer-to-peer call in a direct message or group with at most this many members, and a join past it is refused as full. Lowering it never removes anyone from a running call."), + )) + + h3 class="text-sm font-semibold text-neutral-900" { "Rollout" } + (number_field( + "voice_p2p_rollout_basis_points", + "Rollout (basis points)", + &voice_p2p.rollout_basis_points.to_string(), + Some(0), Some(10000), "1", + Some("Share of users who can start peer-to-peer calls, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."), + )) + (experiment_rollout_country_codes_field( + "voice_p2p_rollout_country_codes", + &voice_p2p.rollout_country_codes, + )) + div class="flex flex-col gap-2" { + (text_input( + "voice_p2p_rollout_salt", + "Rollout Salt", + &voice_p2p.rollout_salt, + VOICE_P2P_DEFAULT_SALT, + )) + p class="text-xs text-neutral-500" { + "Seeds the bucketing hash. Changing it reshuffles which users fall \ + inside the percentage above. Leave it alone to keep the current \ + cohort stable." + } + } + div class="flex flex-col gap-2" { + (textarea_input( + "voice_p2p_included_user_ids", + "Always-on User IDs", + "1500000000000000001\n1500000000000000002", + &included_user_ids, + 4, + false, + )) + (entry_count_hint( + voice_p2p.included_user_ids.len(), + EXPERIMENT_MAX_TARGETED_USERS, + )) + p class="text-xs text-neutral-500" { + "One snowflake per line, or comma separated. These users are targeted \ + regardless of the percentage above. IDs must contain 1 to 20 decimal \ + digits. Invalid entries prevent the save. Blank entries and duplicate \ + IDs are ignored." + } + } + div class="flex flex-col gap-2" { + (checkbox( + "voice_p2p_include_premium_users", + "true", + "Include premium users", + voice_p2p.include_premium_users, + true, + )) + p class="text-xs text-neutral-500" { + "Includes every account with active premium perks, regardless of the \ + percentage above. The never-on list still wins." + } + } + div class="flex flex-col gap-2" { + (textarea_input( + "voice_p2p_included_guild_ids", + "Always-on Guild IDs", + "1500000000000000005\n1500000000000000006", + &voice_p2p.included_guild_ids.join("\n"), + 4, + false, + )) + (entry_count_hint( + voice_p2p.included_guild_ids.len(), + EXPERIMENT_MAX_TARGETED_USERS, + )) + p class="text-xs text-neutral-500" { + "Same format, with guild IDs. Every member of a listed guild is \ + included regardless of the percentage above, unless the user is \ + in the never-on list." + } + } + div class="flex flex-col gap-2" { + (textarea_input( + "voice_p2p_excluded_user_ids", + "Never-on User IDs", + "1500000000000000003\n1500000000000000004", + &excluded_user_ids, + 4, + false, + )) + (entry_count_hint( + voice_p2p.excluded_user_ids.len(), + EXPERIMENT_MAX_TARGETED_USERS, + )) + p class="text-xs text-neutral-500" { + "Same format. Exclusion wins over both the always-on list and the \ + percentage." + } + } + + (form_actions(html! { + (submit_button("Save Peer-to-Peer Voice Configuration")) + })) + } + } + }, + ) +} + fn estimate_low_end_solve_seconds(cost: u32, max_counter: u32) -> f64 { 0.75 * f64::from(cost) * f64::from(max_counter) / 1_050_000.0 } @@ -2094,11 +2272,49 @@ mod tests { assert!(markup.contains("name=\"domain_migration_anonymous_rollout_basis_points\"")); assert!(markup.contains("value=\"250\"")); assert!(markup.contains("name=\"domain_migration_standalone_forwarding\"")); + assert!(markup.contains("name=\"domain_migration_rollout_country_codes\"")); assert!(markup.contains("1 of 1000 stored")); assert!(markup.contains("2 of 1000 stored")); assert!(!markup.contains("at the cap")); } + #[test] + fn voice_p2p_section_shows_the_rollout_and_list_counts() { + let voice_p2p = VoiceP2pConfigResponse { + enabled: true, + config_version: 3, + rollout_basis_points: 250, + rollout_country_codes: vec!["SE".to_owned(), "NO".to_owned()], + max_participants: 3, + included_user_ids: vec!["1500000000000000001".to_owned()], + excluded_user_ids: vec![ + "1500000000000000002".to_owned(), + "1500000000000000003".to_owned(), + ], + ..VoiceP2pConfigResponse::default() + }; + let markup = voice_p2p_section("/admin", "csrf", &voice_p2p).into_string(); + assert!(markup.contains("Peer-to-peer voice")); + assert!(markup.contains("action=update_voice_p2p")); + assert!(markup.contains("name=\"voice_p2p_enabled\"")); + assert!(markup.contains("name=\"voice_p2p_rollout_basis_points\"")); + assert!(markup.contains("value=\"250\"")); + assert!(markup.contains("name=\"voice_p2p_max_participants\"")); + assert!(markup.contains("value=\"3\"")); + assert!(markup.contains("min=\"2\"")); + assert!(markup.contains("max=\"4\"")); + assert!(markup.contains("name=\"voice_p2p_include_premium_users\"")); + assert!(markup.contains("name=\"voice_p2p_included_guild_ids\"")); + assert!(markup.contains("name=\"voice_p2p_rollout_country_codes\"")); + assert!(markup.contains("value=\"SE, NO\"")); + assert!(markup.contains("2 of 250 stored")); + assert!(markup.contains("Config version 3")); + assert!(markup.contains("1 of 1000 stored")); + assert!(markup.contains("2 of 1000 stored")); + assert!(!markup.contains("anonymous_rollout_basis_points")); + assert!(!markup.contains("standalone_forwarding")); + } + #[test] fn push_relay_section_shows_the_consent_toggle() { let accepted = PushRelayConfigResponse { diff --git a/fluxer_admin/tests/api_deserialization.rs b/fluxer_admin/tests/api_deserialization.rs index af7ef37a8..54d52acd1 100644 --- a/fluxer_admin/tests/api_deserialization.rs +++ b/fluxer_admin/tests/api_deserialization.rs @@ -403,6 +403,7 @@ fn deserialize_instance_config_response_with_unknown_keys() { "enabled": true, "config_version": 2, "rollout_basis_points": 2500, + "rollout_country_codes": ["SE"], "rollout_salt": "domain-migration-v1", "included_user_ids": ["1500000000000000001"], "excluded_user_ids": [], @@ -416,6 +417,19 @@ fn deserialize_instance_config_response_with_unknown_keys() { "anonymous_rollout_basis_points": 100, "standalone_forwarding": true }, + "voice_p2p": { + "enabled": true, + "config_version": 4, + "rollout_basis_points": 750, + "rollout_country_codes": ["SE", "NO"], + "rollout_salt": "voice-p2p-v1", + "included_user_ids": ["1500000000000000001"], + "excluded_user_ids": ["1500000000000000002"], + "included_guild_ids": ["1500000000000000005"], + "include_premium_users": true, + "max_participants": 3, + "future_voice_p2p_knob": true + }, "captcha": { "enabled": true, "cost": 5000, @@ -569,10 +583,21 @@ fn deserialize_instance_config_response_with_unknown_keys() { assert!(resp.domain_migration.enabled); assert_eq!(resp.domain_migration.config_version, 2); assert_eq!(resp.domain_migration.rollout_basis_points, 2500); + assert_eq!(resp.domain_migration.rollout_country_codes.len(), 1); assert_eq!(*resp.domain_migration.rollout_salt, "domain-migration-v1"); assert_eq!(resp.domain_migration.included_user_ids.len(), 1); assert_eq!(resp.domain_migration.anonymous_rollout_basis_points, 100); assert!(resp.domain_migration.standalone_forwarding); + assert!(resp.voice_p2p.enabled); + assert_eq!(resp.voice_p2p.config_version, 4); + assert_eq!(resp.voice_p2p.rollout_basis_points, 750); + assert_eq!(resp.voice_p2p.rollout_country_codes.len(), 2); + assert_eq!(*resp.voice_p2p.rollout_salt, "voice-p2p-v1"); + assert_eq!(resp.voice_p2p.included_user_ids.len(), 1); + assert_eq!(resp.voice_p2p.excluded_user_ids.len(), 1); + assert_eq!(resp.voice_p2p.included_guild_ids.len(), 1); + assert!(resp.voice_p2p.include_premium_users); + assert_eq!(resp.voice_p2p.max_participants, 3); assert!(resp.push_relay.relay_consent_accepted); assert!(resp.captcha.enabled); assert_eq!(resp.captcha.max_counter, 1000); diff --git a/fluxer_admin/tests/htmx_acceptance.rs b/fluxer_admin/tests/htmx_acceptance.rs index 1b8d0b3f6..0dfafe0ba 100644 --- a/fluxer_admin/tests/htmx_acceptance.rs +++ b/fluxer_admin/tests/htmx_acceptance.rs @@ -1391,6 +1391,7 @@ async fn mutating_admin_pages_render_usable_csrf_tokens() { "/instance-config?action=update_gateway_rollout", "/instance-config?action=update_sso", "/instance-config?action=update_domain_migration", + "/instance-config?action=update_voice_p2p", "/instance-config?action=update_experiment_delivery", ][..], ), @@ -2424,6 +2425,16 @@ fn instance_config() -> Value { "anonymous_rollout_basis_points": 0, "standalone_forwarding": false }, + "voice_p2p": { + "enabled": false, + "config_version": 0, + "rollout_basis_points": 0, + "rollout_country_codes": [], + "rollout_salt": "voice-p2p-v1", + "included_user_ids": [], + "excluded_user_ids": [], + "max_participants": 2 + }, "experiment_delivery": { "poll_interval_seconds": 300, "poll_jitter_percent": 15 diff --git a/fluxer_api/src/api/Config.ts b/fluxer_api/src/api/Config.ts index 9db9646bb..2d30485f6 100644 --- a/fluxer_api/src/api/Config.ts +++ b/fluxer_api/src/api/Config.ts @@ -318,6 +318,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig { url: master.integrations.voice.url, internalUrl: master.integrations.voice.internal_url, defaultRegion: master.integrations.voice.default_region, + p2pStunUrls: master.integrations.voice.p2p_stun_urls, }, stripe: { enabled: master.integrations.stripe.enabled, diff --git a/fluxer_api/src/api/Tables.ts b/fluxer_api/src/api/Tables.ts index 1f474193f..6dac87ea9 100644 --- a/fluxer_api/src/api/Tables.ts +++ b/fluxer_api/src/api/Tables.ts @@ -338,6 +338,10 @@ import { type UserSettingsRow, type UsersPendingDeletionRow, } from '@app/api/database/types/UserTypes'; +import { + VOICE_P2P_CONNECTION_REPORT_COLUMNS, + type VoiceP2pConnectionReportRow, +} from '@app/api/database/types/VoiceTypes'; import {ATTACHMENT_DECAY_COLUMNS, type AttachmentDecayRow} from '@app/api/types/AttachmentDecayTypes'; import {seconds} from 'itty-time'; @@ -1474,3 +1478,9 @@ export const BillingActionIntents = defineTable({ + name: 'voice_p2p_connection_reports', + columns: VOICE_P2P_CONNECTION_REPORT_COLUMNS, + primaryKey: ['user_id', 'report_id'], + defaultTtlSeconds: seconds('180 days'), +}); diff --git a/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts b/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts index eca870d3d..85bb92b22 100644 --- a/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts +++ b/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts @@ -38,6 +38,7 @@ import { import {DomainMigrationConfigSchema} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas'; import {GatewayRolloutConfigSchema} from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas'; import type {PushRelayConfig, PushRelayConfigUpdateRequest} from '@fluxer/schema/src/domains/admin/PushRelaySchemas'; +import {VoiceP2pConfigSchema} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; import {UserIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas'; import {ExperimentDeliveryConfigSchema} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas'; import type {InstanceBranding} from '@fluxer/schema/src/domains/instance/InstanceSchemas'; @@ -66,6 +67,7 @@ async function buildInstanceConfigResponse(): Promise { gatewayRollout, pushRelay, domainMigration, + voiceP2p, captcha, experimentDelivery, registrationConfig, @@ -76,6 +78,7 @@ async function buildInstanceConfigResponse(): Promise { instanceConfigRepository.getGatewayRolloutConfig(), instanceConfigRepository.getPushRelayConfig(), instanceConfigRepository.getDomainMigrationConfig(), + instanceConfigRepository.getVoiceP2pConfig(), instanceConfigRepository.getCaptchaConfig(), instanceConfigRepository.getExperimentDeliveryConfig(), instanceConfigRepository.getRegistrationConfig(), @@ -113,6 +116,7 @@ async function buildInstanceConfigResponse(): Promise { gateway_rollout: gatewayRollout, push_relay: pushRelay, domain_migration: domainMigration, + voice_p2p: voiceP2p, captcha, experiment_delivery: experimentDelivery, registration: { @@ -396,6 +400,18 @@ export function InstanceConfigAdminController(app: HonoApp) { ); } } + if (data.voice_p2p) { + const patch = omitUndefinedFields(data.voice_p2p); + if (Object.keys(patch).length > 0) { + await instanceConfigRepository.updateVoiceP2pConfig((current) => + VoiceP2pConfigSchema.parse({ + ...current, + ...patch, + config_version: current.config_version + 1, + }), + ); + } + } if (data.captcha) { const patch = omitUndefinedFields(data.captcha); if (Object.keys(patch).length > 0) { diff --git a/fluxer_api/src/api/app/ControllerRegistry.ts b/fluxer_api/src/api/app/ControllerRegistry.ts index 5f2dd82ba..b8d1df90c 100644 --- a/fluxer_api/src/api/app/ControllerRegistry.ts +++ b/fluxer_api/src/api/app/ControllerRegistry.ts @@ -35,6 +35,7 @@ import {ThemeController} from '@app/api/theme/ThemeController'; import type {HonoApp} from '@app/api/types/HonoEnv'; import {UnfurlController} from '@app/api/unfurl/UnfurlController'; import {UserController} from '@app/api/user/controllers/UserController'; +import {VoiceP2pConnectionReportController} from '@app/api/voice/VoiceP2pConnectionReportController'; import {WebhookController} from '@app/api/webhook/WebhookController'; export function registerControllers(routes: HonoApp, config: APIConfig): void { @@ -67,6 +68,7 @@ export function registerControllers(routes: HonoApp, config: APIConfig): void { TestHarnessController(routes); } UserController(routes); + VoiceP2pConnectionReportController(routes); StoreBillingController(routes); WebhookController(routes); OAuth2Controller(routes); diff --git a/fluxer_api/src/api/channel/ChannelMappers.ts b/fluxer_api/src/api/channel/ChannelMappers.ts index cdbcc8c72..559923cc9 100644 --- a/fluxer_api/src/api/channel/ChannelMappers.ts +++ b/fluxer_api/src/api/channel/ChannelMappers.ts @@ -120,6 +120,7 @@ function serializeGuildVoiceChannel(channel: Channel, ctx: ContentWarningCtx): C user_limit: channel.userLimit, voice_connection_limit: channel.voiceConnectionLimit, rtc_region: channel.rtcRegion, + rtc_p2p: channel.rtcP2p, ...serializeContentWarningFields(channel, ctx), rate_limit_per_user: channel.rateLimitPerUser, }; diff --git a/fluxer_api/src/api/channel/PrivateChannelSnapshot.test.ts b/fluxer_api/src/api/channel/PrivateChannelSnapshot.test.ts index c5088af52..7322a8119 100644 --- a/fluxer_api/src/api/channel/PrivateChannelSnapshot.test.ts +++ b/fluxer_api/src/api/channel/PrivateChannelSnapshot.test.ts @@ -40,6 +40,7 @@ function gdmRow(overrides: Partial = {}): ChannelRow { user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: LAST_MESSAGE, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/channel/PrivateChannelSnapshot.ts b/fluxer_api/src/api/channel/PrivateChannelSnapshot.ts index 90472201f..cb8030a0b 100644 --- a/fluxer_api/src/api/channel/PrivateChannelSnapshot.ts +++ b/fluxer_api/src/api/channel/PrivateChannelSnapshot.ts @@ -100,6 +100,7 @@ export function channelRowFromPrivateChannelSnapshot(row: PrivateChannelRow): Ch user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: row.channel_last_message_id ?? null, last_pin_timestamp: row.channel_last_pin_timestamp ?? null, permission_overwrites: null, diff --git a/fluxer_api/src/api/channel/controllers/ChannelController.ts b/fluxer_api/src/api/channel/controllers/ChannelController.ts index 78661ba12..9051bf9ff 100644 --- a/fluxer_api/src/api/channel/controllers/ChannelController.ts +++ b/fluxer_api/src/api/channel/controllers/ChannelController.ts @@ -11,6 +11,7 @@ import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware'; import {SudoModeMiddleware} from '@app/api/middleware/SudoModeMiddleware'; import {RateLimitConfigs} from '@app/api/RateLimitConfig'; import type {HonoApp, HonoEnv} from '@app/api/types/HonoEnv'; +import {lookupGeoip} from '@app/api/utils/IpUtils'; import {Validator} from '@app/api/Validator'; import {ANNOUNCEMENT_CONVERTIBLE_CHANNEL_TYPES} from '@fluxer/constants/src/ChannelConstants'; import {TEXT_THREAD_PARENT_CHANNEL_TYPES, THREAD_ONLY_CHANNEL_TYPES} from '@fluxer/constants/src/ThreadConstants'; @@ -192,6 +193,7 @@ export function ChannelController(app: HonoApp) { const requestCache = ctx.get('requestCache'); const auditLogReason = ctx.get('auditLogReason') ?? null; const channelRequestService = ctx.get('channelRequestService'); + const {countryCode} = await lookupGeoip(ctx.req.raw); return ctx.json( await channelRequestService.updateChannel({ viewer: viewerFromCtx(ctx), @@ -202,6 +204,7 @@ export function ChannelController(app: HonoApp) { requestCache, auditLogReason, typeConversion, + countryCode, }), ); }, diff --git a/fluxer_api/src/api/channel/repositories/ChannelDataRepository.test.ts b/fluxer_api/src/api/channel/repositories/ChannelDataRepository.test.ts index 9b6cb2b18..421e0871b 100644 --- a/fluxer_api/src/api/channel/repositories/ChannelDataRepository.test.ts +++ b/fluxer_api/src/api/channel/repositories/ChannelDataRepository.test.ts @@ -61,6 +61,7 @@ function makeChannelRow(channelId: ChannelID): ChannelRow { user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/channel/repositories/ThreadRepository.test.ts b/fluxer_api/src/api/channel/repositories/ThreadRepository.test.ts index 2835c77d6..8ad754c67 100644 --- a/fluxer_api/src/api/channel/repositories/ThreadRepository.test.ts +++ b/fluxer_api/src/api/channel/repositories/ThreadRepository.test.ts @@ -128,6 +128,7 @@ function channelRow(channelId: ChannelID, type: number, overrides: Partial { 'user_limit', 'voice_connection_limit', 'rtc_region', + 'rtc_p2p', 'last_message_id', 'last_pin_timestamp', 'permission_overwrites', diff --git a/fluxer_api/src/api/channel/services/ChannelDataService.ts b/fluxer_api/src/api/channel/services/ChannelDataService.ts index 85d2f9b6b..55cf99495 100644 --- a/fluxer_api/src/api/channel/services/ChannelDataService.ts +++ b/fluxer_api/src/api/channel/services/ChannelDataService.ts @@ -149,6 +149,7 @@ export class ChannelDataService { requestCache, auditLogReason, typeConversion, + countryCode, }: { userId: UserID; viewer: ThreadViewer; @@ -158,6 +159,7 @@ export class ChannelDataService { requestCache: RequestCache; auditLogReason: string | null; typeConversion?: ChannelTypeConversion | null; + countryCode?: string | null; }): Promise { const authChannel = await this.auth.getChannelAuthenticated({userId, channelId, viewer, skipNsfwValidation: true}); const {channel} = authChannel; @@ -227,6 +229,9 @@ export class ChannelDataService { if (guildChannelData.rtc_region !== undefined) { channelUpdateData.rtc_region = guildChannelData.rtc_region ?? null; } + if (guildChannelData.rtc_p2p !== undefined) { + channelUpdateData.rtc_p2p = guildChannelData.rtc_p2p; + } if (guildChannelData.icon !== undefined) { channelUpdateData.icon = guildChannelData.icon ?? null; } @@ -246,6 +251,7 @@ export class ChannelDataService { requestCache, auditLogReason, typeConversion, + countryCode, }); } } diff --git a/fluxer_api/src/api/channel/services/ChannelRequestService.ts b/fluxer_api/src/api/channel/services/ChannelRequestService.ts index 050d04d68..7b69ba62d 100644 --- a/fluxer_api/src/api/channel/services/ChannelRequestService.ts +++ b/fluxer_api/src/api/channel/services/ChannelRequestService.ts @@ -122,6 +122,7 @@ export class ChannelRequestService { requestCache: RequestCache; auditLogReason: string | null; typeConversion?: ChannelTypeConversion | null; + countryCode?: string | null; }): Promise { if (!('available_tags' in params.data) || params.data.available_tags === undefined) { return this.applyChannelUpdate(params); @@ -138,6 +139,7 @@ export class ChannelRequestService { requestCache: RequestCache; auditLogReason: string | null; typeConversion?: ChannelTypeConversion | null; + countryCode?: string | null; }): Promise { const channel = await this.channelService.channelData.editChannel({ userId: params.userId, @@ -148,6 +150,7 @@ export class ChannelRequestService { requestCache: params.requestCache, auditLogReason: params.auditLogReason, typeConversion: params.typeConversion, + countryCode: params.countryCode, }); return this.maskedChannelResponse(channel, params); } diff --git a/fluxer_api/src/api/channel/services/PersonalNotesChannelRepair.ts b/fluxer_api/src/api/channel/services/PersonalNotesChannelRepair.ts index 6a10c9c65..d7a7bdd7d 100644 --- a/fluxer_api/src/api/channel/services/PersonalNotesChannelRepair.ts +++ b/fluxer_api/src/api/channel/services/PersonalNotesChannelRepair.ts @@ -36,6 +36,7 @@ function buildPersonalNotesChannelRow(userId: UserID): ChannelRow { user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/channel/services/channel_data/ChannelOperationsService.ts b/fluxer_api/src/api/channel/services/channel_data/ChannelOperationsService.ts index 577cccc07..90f8130d6 100644 --- a/fluxer_api/src/api/channel/services/channel_data/ChannelOperationsService.ts +++ b/fluxer_api/src/api/channel/services/channel_data/ChannelOperationsService.ts @@ -32,6 +32,7 @@ import { type ThreadViewer, viewerActive, } from '@app/api/experiment/ChannelThreadsGate'; +import {resolveExperimentTargeting} from '@app/api/experiment/ExperimentTargeting'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import {mapGuildToGuildResponse} from '@app/api/guild/GuildModel'; import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; @@ -48,6 +49,7 @@ import {Logger} from '@app/api/Logger'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; +import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons'; import type {Channel} from '@app/api/models/Channel'; import {ChannelPermissionOverwrite} from '@app/api/models/ChannelPermissionOverwrite'; import type {ThreadState} from '@app/api/models/ThreadState'; @@ -81,6 +83,7 @@ import {CannotExecuteOnDmError} from '@fluxer/errors/src/domains/core/CannotExec import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError'; import {resolveLimit} from '@fluxer/limits/src/LimitResolver'; +import {resolveVoiceP2pAssignment} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; import {ChannelNameType} from '@fluxer/schema/src/primitives/ChannelValidators'; import type {ICacheService} from '@pkgs/cache/src/ICacheService'; import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; @@ -105,6 +108,7 @@ export interface ChannelUpdateData { deny?: bigint; }> | null; rtc_region?: string | null; + rtc_p2p?: boolean; icon?: string | null; owner_id?: bigint | null; nicks?: Record | null; @@ -137,6 +141,21 @@ export class ChannelOperationsService { private snowflakeService: ISnowflakeService, ) {} + private async assertVoiceP2pAssigned(userId: UserID, countryCode: string | null): Promise { + const config = await getInstanceConfigRepository().getVoiceP2pConfig(); + const user = config.enabled ? await this.userRepository.findUnique(userId) : null; + if ( + user === null || + !resolveVoiceP2pAssignment( + config, + userId.toString(), + await resolveExperimentTargeting(user, countryCode, [config]), + ).enabled + ) { + throw InputValidationError.fromCode('rtc_p2p', ValidationErrorCodes.GUILD_FEATURE_NOT_TOGGLEABLE); + } + } + async getChannel({ userId, viewer, @@ -183,6 +202,7 @@ export class ChannelOperationsService { auditLogReason, typeConversion, threadParent, + countryCode, }: { userId: UserID; viewer: ThreadViewer; @@ -193,6 +213,7 @@ export class ChannelOperationsService { auditLogReason: string | null; typeConversion?: ChannelTypeConversion | null; threadParent?: ThreadParentSettingsInput | null; + countryCode?: string | null; }): Promise { const {channel, guild, checkPermission} = await this.channelAuthService.getChannelAuthenticated({ userId, @@ -267,6 +288,9 @@ export class ChannelOperationsService { } } } + if (data.rtc_p2p === true && !channel.rtcP2p && channel.type === ChannelTypes.GUILD_VOICE) { + await this.assertVoiceP2pAssigned(userId, countryCode ?? null); + } const previousPermissionOverwrites = channel.permissionOverwrites; let permissionOverwrites = channel.permissionOverwrites; if (data.permission_overwrites !== undefined) { @@ -378,6 +402,7 @@ export class ChannelOperationsService { data.rtc_region !== undefined && channel.type === ChannelTypes.GUILD_VOICE ? data.rtc_region : channel.rtcRegion, + rtc_p2p: data.rtc_p2p !== undefined && channel.type === ChannelTypes.GUILD_VOICE ? data.rtc_p2p : channel.rtcP2p, permission_overwrites: new Map( Array.from(permissionOverwrites.entries()).map(([targetId, overwrite]) => [ targetId, diff --git a/fluxer_api/src/api/channel/services/message/MessageProcessingService.test.ts b/fluxer_api/src/api/channel/services/message/MessageProcessingService.test.ts index 8c8e145b6..88c3892d6 100644 --- a/fluxer_api/src/api/channel/services/message/MessageProcessingService.test.ts +++ b/fluxer_api/src/api/channel/services/message/MessageProcessingService.test.ts @@ -44,6 +44,7 @@ function dmChannelRow(lastMessageId: MessageID | null): ChannelRow { user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: lastMessageId, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/channel/services/thread/ThreadCreationService.ts b/fluxer_api/src/api/channel/services/thread/ThreadCreationService.ts index fac1e2ada..74b620e2d 100644 --- a/fluxer_api/src/api/channel/services/thread/ThreadCreationService.ts +++ b/fluxer_api/src/api/channel/services/thread/ThreadCreationService.ts @@ -453,6 +453,7 @@ export class ThreadCreationService { user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/channel/tests/ThreadCrossCuttingHelpers.test.ts b/fluxer_api/src/api/channel/tests/ThreadCrossCuttingHelpers.test.ts index 786a9ea5d..11c459bd2 100644 --- a/fluxer_api/src/api/channel/tests/ThreadCrossCuttingHelpers.test.ts +++ b/fluxer_api/src/api/channel/tests/ThreadCrossCuttingHelpers.test.ts @@ -38,6 +38,7 @@ function channel(id: bigint, type: number, overrides: Partial = {}): user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/config/APIConfig.ts b/fluxer_api/src/api/config/APIConfig.ts index 3e76b328d..3f578e6b0 100644 --- a/fluxer_api/src/api/config/APIConfig.ts +++ b/fluxer_api/src/api/config/APIConfig.ts @@ -192,6 +192,7 @@ export interface APIConfig { latitude: number; longitude: number; }; + p2pStunUrls?: Array; }; stripe: { enabled: boolean; diff --git a/fluxer_api/src/api/database/types/ChannelTypes.ts b/fluxer_api/src/api/database/types/ChannelTypes.ts index 6393e0e42..2b645a115 100644 --- a/fluxer_api/src/api/database/types/ChannelTypes.ts +++ b/fluxer_api/src/api/database/types/ChannelTypes.ts @@ -39,6 +39,7 @@ export interface ChannelRow { user_limit: Nullish; voice_connection_limit: Nullish; rtc_region: Nullish; + rtc_p2p: Nullish; last_message_id: Nullish; last_pin_timestamp: Nullish; permission_overwrites: Nullish>; @@ -156,6 +157,7 @@ export const CHANNEL_COLUMNS = [ 'user_limit', 'voice_connection_limit', 'rtc_region', + 'rtc_p2p', 'last_message_id', 'last_pin_timestamp', 'permission_overwrites', diff --git a/fluxer_api/src/api/database/types/VoiceTypes.ts b/fluxer_api/src/api/database/types/VoiceTypes.ts index d76c40bde..9fdee5cd0 100644 --- a/fluxer_api/src/api/database/types/VoiceTypes.ts +++ b/fluxer_api/src/api/database/types/VoiceTypes.ts @@ -1,5 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes'; + export interface VoiceRegionRow { id: string; name: string; @@ -65,3 +67,43 @@ export const VOICE_SERVER_COLUMNS = [ 'created_at', 'updated_at', ] as const satisfies ReadonlyArray; + +export interface VoiceP2pConnectionReportRow { + user_id: UserID; + report_id: bigint; + reported_at: Date; + channel_id: ChannelID; + guild_id: GuildID | null; + participant_count: number; + outcome: string; + local_candidate_type: string | null; + remote_candidate_type: string | null; + ip_family: string | null; + protocol: string | null; + setup_ms: number | null; + ice_restarted: boolean; + country: string | null; + ip: string | null; + client_platform: string | null; + client_os: string | null; +} + +export const VOICE_P2P_CONNECTION_REPORT_COLUMNS = [ + 'user_id', + 'report_id', + 'reported_at', + 'channel_id', + 'guild_id', + 'participant_count', + 'outcome', + 'local_candidate_type', + 'remote_candidate_type', + 'ip_family', + 'protocol', + 'setup_ms', + 'ice_restarted', + 'country', + 'ip', + 'client_platform', + 'client_os', +] as const satisfies ReadonlyArray; diff --git a/fluxer_api/src/api/experiment/ExperimentController.ts b/fluxer_api/src/api/experiment/ExperimentController.ts index 23a178879..d500a5403 100644 --- a/fluxer_api/src/api/experiment/ExperimentController.ts +++ b/fluxer_api/src/api/experiment/ExperimentController.ts @@ -8,9 +8,11 @@ import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware'; import {RateLimitConfigs} from '@app/api/RateLimitConfig'; import type {HonoApp} from '@app/api/types/HonoEnv'; import {entityTagMatches} from '@app/api/utils/EntityTag'; +import {lookupGeoip} from '@app/api/utils/IpUtils'; import {Headers as HttpHeaders} from '@fluxer/constants/src/Headers'; import {resolveChannelThreadsAssignment} from '@fluxer/schema/src/domains/admin/ChannelThreadsSchemas'; import {resolveDomainMigrationAssignment} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas'; +import {resolveVoiceP2pAssignment} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; import {ExperimentAssignmentsResponse} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas'; export function ExperimentController(app: HonoApp) { @@ -30,20 +32,26 @@ export function ExperimentController(app: HonoApp) { }), async (ctx) => { const instanceConfigRepository = ctx.get('instanceConfigRepository'); - const [delivery, domainMigrationConfig, channelThreadsConfig] = await Promise.all([ + const [delivery, domainMigrationConfig, channelThreadsConfig, voiceP2pConfig, geoip] = await Promise.all([ instanceConfigRepository.getExperimentDeliveryConfig(), instanceConfigRepository.getDomainMigrationConfig(), instanceConfigRepository.getCompiledChannelThreadsConfig(), + instanceConfigRepository.getVoiceP2pConfig(), + lookupGeoip(ctx.req.raw), ]); const user = ctx.get('user'); const userId = user.id.toString(); - const targeting = await resolveExperimentTargeting(user, domainMigrationConfig); + const targeting = await resolveExperimentTargeting(user, geoip.countryCode, [ + domainMigrationConfig, + voiceP2pConfig, + ]); const body: ExperimentAssignmentsResponse = { poll_interval_seconds: delivery.poll_interval_seconds, poll_jitter_percent: delivery.poll_jitter_percent, assignments: { domain_migration: resolveDomainMigrationAssignment(domainMigrationConfig, userId, targeting), plutonium_page: {enabled: true}, + voice_p2p: resolveVoiceP2pAssignment(voiceP2pConfig, userId, targeting), }, }; const channelThreads = resolveChannelThreadsAssignment(channelThreadsConfig, userId); diff --git a/fluxer_api/src/api/experiment/ExperimentTargeting.ts b/fluxer_api/src/api/experiment/ExperimentTargeting.ts index 45cb6632b..695622f29 100644 --- a/fluxer_api/src/api/experiment/ExperimentTargeting.ts +++ b/fluxer_api/src/api/experiment/ExperimentTargeting.ts @@ -13,11 +13,12 @@ const NO_GUILDS: ReadonlySet = new Set(); export async function resolveExperimentTargeting( user: User, - config: TargetableExperimentConfig, + countryCode: string | null, + configs: ReadonlyArray, ): Promise { - const needsGuilds = config.enabled && config.included_guild_ids.length > 0; + const needsGuilds = configs.some((config) => config.enabled && config.included_guild_ids.length > 0); const memberGuildIds = needsGuilds ? new Set((await getUserRepository().getUserGuildIds(user.id)).map((guildId) => guildId.toString())) : NO_GUILDS; - return {memberGuildIds, premium: !user.isBot && user.isPremium()}; + return {memberGuildIds, premium: !user.isBot && user.isPremium(), countryCode}; } diff --git a/fluxer_api/src/api/experiment/tests/ExperimentController.test.ts b/fluxer_api/src/api/experiment/tests/ExperimentController.test.ts index d10164642..a68c55ca9 100644 --- a/fluxer_api/src/api/experiment/tests/ExperimentController.test.ts +++ b/fluxer_api/src/api/experiment/tests/ExperimentController.test.ts @@ -14,6 +14,7 @@ import { DEFAULT_DOMAIN_MIGRATION_CONFIG, INERT_DOMAIN_MIGRATION_ASSIGNMENT, } from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas'; +import {DEFAULT_VOICE_P2P_CONFIG, INERT_VOICE_P2P_ASSIGNMENT} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; import { DEFAULT_EXPERIMENT_POLL_INTERVAL_SECONDS, DEFAULT_EXPERIMENT_POLL_JITTER_PERCENT, @@ -22,8 +23,29 @@ import { readChannelThreadsAssignment, readDomainMigrationAssignment, readPlutoniumPageAssignment, + readVoiceP2pAssignment, } from '@fluxer/schema/src/domains/experiment/ExperimentSchemas'; -import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; +import type {GeoipResult} from '@pkgs/geoip/src/GeoipLookup'; +import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'; + +const {lookupGeoipMock} = vi.hoisted(() => ({ + lookupGeoipMock: vi.fn(), +})); + +vi.mock('@app/api/utils/IpUtils', async (importOriginal) => ({ + ...(await importOriginal()), + lookupGeoip: lookupGeoipMock, +})); + +function geoipCountry(countryCode: string | null): GeoipResult { + return { + countryCode, + normalizedIp: '203.0.113.10', + city: null, + region: null, + countryName: null, + }; +} const NOT_MODIFIED = 304; const ENDPOINT = '/experiments'; @@ -37,6 +59,8 @@ describe('GET /experiments', () => { beforeEach(async () => { await harness.reset(); + lookupGeoipMock.mockReset(); + lookupGeoipMock.mockResolvedValue(geoipCountry(null)); }); afterAll(async () => { @@ -59,6 +83,7 @@ describe('GET /experiments', () => { domain_migration: INERT_DOMAIN_MIGRATION_ASSIGNMENT, channel_threads: {active: true, config_version: 0}, plutonium_page: {enabled: true}, + voice_p2p: INERT_VOICE_P2P_ASSIGNMENT, }, }); }); @@ -72,6 +97,15 @@ describe('GET /experiments', () => { expect(readDomainMigrationAssignment(body).enabled).toBe(false); }); + it('populates the voice p2p assignment key even when the rollout is disabled', async () => { + const account = await createTestAccount(harness); + + const body = await createBuilder(harness, account.token).get(ENDPOINT).execute(); + + expect(Object.hasOwn(body.assignments, 'voice_p2p')).toBe(true); + expect(readVoiceP2pAssignment(body).enabled).toBe(false); + }); + it('serves the plutonium page to everyone and ignores a leftover rollout row', async () => { const account = await createTestAccount(harness); await getInstanceConfigRepository().setConfig( @@ -121,7 +155,65 @@ describe('GET /experiments', () => { expect(body.assignments.domain_migration).toEqual({enabled: false}); }); - it('enrols members of an included guild in domain migration and leaves everyone else out', async () => { + it('resolves the voice p2p caller through the allowlist and the exclusion list', async () => { + const targeted = await createTestAccount(harness); + const untargeted = await createTestAccount(harness); + const excluded = await createTestAccount(harness); + await getInstanceConfigRepository().setVoiceP2pConfig({ + ...DEFAULT_VOICE_P2P_CONFIG, + enabled: true, + rollout_basis_points: 0, + included_user_ids: [targeted.userId, excluded.userId], + excluded_user_ids: [excluded.userId], + }); + + const targetedBody = await createBuilder(harness, targeted.token) + .get(ENDPOINT) + .execute(); + expect(targetedBody.assignments.voice_p2p).toEqual({enabled: true, max_participants: 2}); + + const untargetedBody = await createBuilder(harness, untargeted.token) + .get(ENDPOINT) + .execute(); + expect(untargetedBody.assignments.voice_p2p).toEqual({enabled: false, max_participants: 2}); + + const excludedBody = await createBuilder(harness, excluded.token) + .get(ENDPOINT) + .execute(); + expect(excludedBody.assignments.voice_p2p).toEqual({enabled: false, max_participants: 2}); + }); + + it('limits the percentage rollout of every experiment to the listed request countries', async () => { + const account = await createTestAccount(harness); + const included = await createTestAccount(harness); + const repository = getInstanceConfigRepository(); + const rollout = {enabled: true, rollout_basis_points: 10000, rollout_country_codes: ['SE', 'NO']}; + await repository.setDomainMigrationConfig({...DEFAULT_DOMAIN_MIGRATION_CONFIG, ...rollout}); + await repository.setVoiceP2pConfig({ + ...DEFAULT_VOICE_P2P_CONFIG, + ...rollout, + included_user_ids: [included.userId], + }); + + for (const [countryCode, expected] of [ + ['SE', true], + ['BR', false], + [null, false], + ] as const) { + lookupGeoipMock.mockResolvedValue(geoipCountry(countryCode)); + const body = await createBuilder(harness, account.token).get(ENDPOINT).execute(); + expect(body.assignments.domain_migration).toEqual({enabled: expected}); + expect(body.assignments.voice_p2p).toEqual({enabled: expected, max_participants: 2}); + } + + lookupGeoipMock.mockResolvedValue(geoipCountry('BR')); + const includedBody = await createBuilder(harness, included.token) + .get(ENDPOINT) + .execute(); + expect(includedBody.assignments.voice_p2p).toEqual({enabled: true, max_participants: 2}); + }); + + it('enrols members of an included guild in every experiment and leaves everyone else out', async () => { const owner = await createTestAccount(harness); const member = await createTestAccount(harness); const outsider = await createTestAccount(harness); @@ -129,21 +221,29 @@ describe('GET /experiments', () => { const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!); const invite = await createChannelInvite(harness, owner.token, systemChannel.id); await acceptInvite(harness, member.token, invite.code); - await getInstanceConfigRepository().setDomainMigrationConfig({ + const repository = getInstanceConfigRepository(); + await repository.setDomainMigrationConfig({ ...DEFAULT_DOMAIN_MIGRATION_CONFIG, enabled: true, included_guild_ids: [guild.id], }); + await repository.setVoiceP2pConfig({ + ...DEFAULT_VOICE_P2P_CONFIG, + enabled: true, + included_guild_ids: [guild.id], + }); const memberBody = await createBuilder(harness, member.token) .get(ENDPOINT) .execute(); expect(memberBody.assignments.domain_migration).toEqual({enabled: true}); + expect(memberBody.assignments.voice_p2p).toEqual({enabled: true, max_participants: 2}); const outsiderBody = await createBuilder(harness, outsider.token) .get(ENDPOINT) .execute(); expect(outsiderBody.assignments.domain_migration).toEqual({enabled: false}); + expect(outsiderBody.assignments.voice_p2p).toEqual({enabled: false, max_participants: 2}); }); it('enrols premium users, subscription and lifetime alike, when the switch is on', async () => { @@ -152,11 +252,17 @@ describe('GET /experiments', () => { const free = await createTestAccount(harness); await grantPremium(harness, subscriber.userId, UserPremiumTypes.SUBSCRIPTION); await grantPremium(harness, visionary.userId, UserPremiumTypes.LIFETIME); - await getInstanceConfigRepository().setDomainMigrationConfig({ + const repository = getInstanceConfigRepository(); + await repository.setDomainMigrationConfig({ ...DEFAULT_DOMAIN_MIGRATION_CONFIG, enabled: true, include_premium_users: true, }); + await repository.setVoiceP2pConfig({ + ...DEFAULT_VOICE_P2P_CONFIG, + enabled: true, + include_premium_users: true, + }); for (const [account, expected] of [ [subscriber, true], [visionary, true], @@ -164,24 +270,63 @@ describe('GET /experiments', () => { ] as const) { const body = await createBuilder(harness, account.token).get(ENDPOINT).execute(); expect(body.assignments.domain_migration).toEqual({enabled: expected}); + expect(body.assignments.voice_p2p).toEqual({enabled: expected, max_participants: 2}); } }); - it('stores the guild ids and premium switch an admin sets for domain migration', async () => { + it('stores the guild ids, countries and premium switch an admin sets for each experiment', async () => { const admin = await setUserACLs(harness, await createTestAccount(harness), [ AdminACLs.AUTHENTICATE, AdminACLs.INSTANCE_CONFIG_VIEW, AdminACLs.INSTANCE_CONFIG_UPDATE, ]); const guildIds = ['1500000000000000001', '1500000000000000002']; - const body = await createBuilder<{ - domain_migration: {included_guild_ids: Array; include_premium_users: boolean}; - }>(harness, admin.token) + const countryCodes = ['SE', 'NO']; + const section = {included_guild_ids: guildIds, include_premium_users: true, rollout_country_codes: countryCodes}; + const body = await createBuilder< + Record< + 'domain_migration' | 'voice_p2p', + {included_guild_ids: Array; include_premium_users: boolean; rollout_country_codes: Array} + > + >(harness, admin.token) .patch('/admin/instance/config') - .body({domain_migration: {included_guild_ids: guildIds, include_premium_users: true}}) + .body({domain_migration: section, voice_p2p: section}) + .execute(); + for (const stored of [body.domain_migration, body.voice_p2p]) { + expect(stored.included_guild_ids).toEqual(guildIds); + expect(stored.include_premium_users).toBe(true); + expect(stored.rollout_country_codes).toEqual(countryCodes); + } + }); + + it.each([ + {name: 'lowercase', countryCodes: ['se']}, + {name: 'duplicated', countryCodes: ['SE', 'SE']}, + {name: 'malformed', countryCodes: ['SWE']}, + ])('rejects $name rollout country codes from an admin', async ({countryCodes}) => { + const admin = await setUserACLs(harness, await createTestAccount(harness), [ + AdminACLs.AUTHENTICATE, + AdminACLs.INSTANCE_CONFIG_VIEW, + AdminACLs.INSTANCE_CONFIG_UPDATE, + ]); + await createBuilder(harness, admin.token) + .patch('/admin/instance/config') + .body({voice_p2p: {rollout_country_codes: countryCodes}}) + .expect(HTTP_STATUS.BAD_REQUEST) + .execute(); + }); + + it.each([1, 5])('rejects a voice p2p max_participants of %i from an admin', async (maxParticipants) => { + const admin = await setUserACLs(harness, await createTestAccount(harness), [ + AdminACLs.AUTHENTICATE, + AdminACLs.INSTANCE_CONFIG_VIEW, + AdminACLs.INSTANCE_CONFIG_UPDATE, + ]); + await createBuilder(harness, admin.token) + .patch('/admin/instance/config') + .body({voice_p2p: {max_participants: maxParticipants}}) + .expect(HTTP_STATUS.BAD_REQUEST) .execute(); - expect(body.domain_migration.included_guild_ids).toEqual(guildIds); - expect(body.domain_migration.include_premium_users).toBe(true); }); it('revalidates with a strong etag and answers 304 when nothing changed', async () => { @@ -336,6 +481,40 @@ describe('GET /experiments', () => { expect(afterUndefined.domain_migration).toMatchObject({config_version: 1, enabled: true}); }); + it('bumps the voice p2p config version on every admin update without the client sending one', async () => { + const admin = await setUserACLs(harness, await createTestAccount(harness), [ + AdminACLs.AUTHENTICATE, + AdminACLs.INSTANCE_CONFIG_VIEW, + AdminACLs.INSTANCE_CONFIG_UPDATE, + ]); + + const afterFirst = await createBuilder<{voice_p2p: {config_version: number; enabled: boolean}}>( + harness, + admin.token, + ) + .patch('/admin/instance/config') + .body({voice_p2p: {enabled: true, included_user_ids: [admin.userId]}}) + .execute(); + expect(afterFirst.voice_p2p).toMatchObject({config_version: 1, enabled: true}); + + const afterSecond = await createBuilder<{ + voice_p2p: {config_version: number; rollout_basis_points: number}; + }>(harness, admin.token) + .patch('/admin/instance/config') + .body({voice_p2p: {rollout_basis_points: 2500, max_participants: 4}}) + .execute(); + expect(afterSecond.voice_p2p).toMatchObject({config_version: 2, rollout_basis_points: 2500, max_participants: 4}); + + const afterEmpty = await createBuilder<{voice_p2p: {config_version: number}}>(harness, admin.token) + .patch('/admin/instance/config') + .body({voice_p2p: {}}) + .execute(); + expect(afterEmpty.voice_p2p).toMatchObject({config_version: 2}); + + const body = await createBuilder(harness, admin.token).get(ENDPOINT).execute(); + expect(body.assignments.voice_p2p).toEqual({enabled: true, max_participants: 4}); + }); + it('no longer exposes or accepts plutonium page and channel threads settings in the admin config', async () => { const admin = await setUserACLs(harness, await createTestAccount(harness), [ AdminACLs.AUTHENTICATE, diff --git a/fluxer_api/src/api/guild/services/channel/ChannelOperationsService.ts b/fluxer_api/src/api/guild/services/channel/ChannelOperationsService.ts index cbcaf01db..bad35e8a8 100644 --- a/fluxer_api/src/api/guild/services/channel/ChannelOperationsService.ts +++ b/fluxer_api/src/api/guild/services/channel/ChannelOperationsService.ts @@ -238,6 +238,7 @@ export class ChannelOperationsService { ? (params.data.voice_connection_limit ?? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT) : null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: permissionOverwrites, diff --git a/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts b/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts index 5761c2816..5b4a51fd7 100644 --- a/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts +++ b/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts @@ -881,6 +881,7 @@ export class GuildOperationsService { user_limit: bitrate !== null ? 0 : null, voice_connection_limit: bitrate !== null ? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT : null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: null, @@ -1113,6 +1114,7 @@ export class GuildOperationsService { ? (channel.voice_connection_limit ?? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT) : null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: permissionOverwrites, @@ -1163,6 +1165,7 @@ export class GuildOperationsService { user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/guild/tests/GuildChannelManagement.test.ts b/fluxer_api/src/api/guild/tests/GuildChannelManagement.test.ts index b1e71fb7c..8d658e786 100644 --- a/fluxer_api/src/api/guild/tests/GuildChannelManagement.test.ts +++ b/fluxer_api/src/api/guild/tests/GuildChannelManagement.test.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; +import {createChannelID} from '@app/api/BrandedTypes'; import { addMemberRole, createChannel, @@ -10,11 +11,14 @@ import { updateChannel, } from '@app/api/channel/tests/ChannelTestUtils'; import {createGuild, getGuildChannels} from '@app/api/guild/tests/GuildTestUtils'; +import {getChannelRepository, getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import {HTTP_STATUS} from '@app/api/test/TestConstants'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; import {GuildFeatures} from '@fluxer/constants/src/GuildConstants'; +import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; +import {DEFAULT_VOICE_P2P_CONFIG, type VoiceP2pConfig} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas'; import {afterEach, beforeEach, describe, expect, test} from 'vitest'; @@ -26,6 +30,9 @@ describe('Guild Channel Management', () => { afterEach(async () => { await harness?.shutdown(); }); + async function setVoiceP2pConfig(config: Partial): Promise { + await getInstanceConfigRepository().setVoiceP2pConfig({...DEFAULT_VOICE_P2P_CONFIG, ...config}); + } async function addGuildFeaturesForTesting(guildId: string, features: Array): Promise { await createBuilder<{ success: boolean; @@ -559,6 +566,98 @@ describe('Guild Channel Management', () => { .execute(); expect(data.voice_connection_limit).toBe(10); }); + test('should default rtc_p2p to false and update it on a voice channel', async () => { + const account = await createTestAccount(harness); + const guild = await createGuild(harness, account.token, 'Test Guild'); + const voiceChannel = await createChannel( + harness, + account.token, + guild.id, + 'voice-channel', + ChannelTypes.GUILD_VOICE, + ); + expect(voiceChannel.rtc_p2p).toBe(false); + await setVoiceP2pConfig({enabled: true, included_user_ids: [account.userId]}); + const enabled = await createBuilder(harness, account.token) + .patch(`/channels/${voiceChannel.id}`) + .body({type: ChannelTypes.GUILD_VOICE, rtc_p2p: true}) + .execute(); + expect(enabled.rtc_p2p).toBe(true); + const untouched = await createBuilder(harness, account.token) + .patch(`/channels/${voiceChannel.id}`) + .body({type: ChannelTypes.GUILD_VOICE, user_limit: 4}) + .execute(); + expect(untouched.rtc_p2p).toBe(true); + const disabled = await createBuilder(harness, account.token) + .patch(`/channels/${voiceChannel.id}`) + .body({type: ChannelTypes.GUILD_VOICE, rtc_p2p: false}) + .execute(); + expect(disabled.rtc_p2p).toBe(false); + }); + test('should refuse enabling rtc_p2p outside the voice_p2p experiment', async () => { + const account = await createTestAccount(harness); + const guild = await createGuild(harness, account.token, 'Test Guild'); + const voiceChannel = await createChannel( + harness, + account.token, + guild.id, + 'voice-channel', + ChannelTypes.GUILD_VOICE, + ); + for (const config of [{included_user_ids: [account.userId]}, {enabled: true}]) { + await setVoiceP2pConfig(config); + const refusal = await createBuilder<{errors?: Array<{path: string; code: string}>}>(harness, account.token) + .patch(`/channels/${voiceChannel.id}`) + .body({type: ChannelTypes.GUILD_VOICE, rtc_p2p: true}) + .expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_FORM_BODY') + .execute(); + expect(refusal.errors).toContainEqual( + expect.objectContaining({path: 'rtc_p2p', code: ValidationErrorCodes.GUILD_FEATURE_NOT_TOGGLEABLE}), + ); + } + const stored = await getChannelRepository().findUnique(createChannelID(BigInt(voiceChannel.id))); + expect(stored?.rtcP2p).toBe(false); + await setVoiceP2pConfig({enabled: true, included_user_ids: [account.userId]}); + await createBuilder(harness, account.token) + .patch(`/channels/${voiceChannel.id}`) + .body({type: ChannelTypes.GUILD_VOICE, rtc_p2p: true}) + .execute(); + await setVoiceP2pConfig({}); + const disabled = await createBuilder(harness, account.token) + .patch(`/channels/${voiceChannel.id}`) + .body({type: ChannelTypes.GUILD_VOICE, rtc_p2p: false}) + .execute(); + expect(disabled.rtc_p2p).toBe(false); + }); + test('should ignore rtc_p2p on a text channel', async () => { + const account = await createTestAccount(harness); + const guild = await createGuild(harness, account.token, 'Test Guild'); + const textChannel = await createChannel(harness, account.token, guild.id, 'text-channel'); + const data = await createBuilder(harness, account.token) + .patch(`/channels/${textChannel.id}`) + .body({type: ChannelTypes.GUILD_TEXT, rtc_p2p: true}) + .execute(); + expect(data.rtc_p2p).toBeUndefined(); + const stored = await getChannelRepository().findUnique(createChannelID(BigInt(textChannel.id))); + expect(stored?.rtcP2p).toBe(false); + }); + test('should require MANAGE_CHANNELS to update rtc_p2p', async () => { + const {owner, members, guild} = await setupTestGuildWithMembers(harness, 1); + const voiceChannel = await createChannel( + harness, + owner.token, + guild.id, + 'voice-channel', + ChannelTypes.GUILD_VOICE, + ); + await createBuilder(harness, members[0].token) + .patch(`/channels/${voiceChannel.id}`) + .body({type: ChannelTypes.GUILD_VOICE, rtc_p2p: true}) + .expect(HTTP_STATUS.FORBIDDEN) + .execute(); + const stored = await getChannelRepository().findUnique(createChannelID(BigInt(voiceChannel.id))); + expect(stored?.rtcP2p).toBe(false); + }); test('should set unlimited user capacity with zero', async () => { const account = await createTestAccount(harness); const guild = await createGuild(harness, account.token, 'Test Guild'); diff --git a/fluxer_api/src/api/infrastructure/KVThreadAutoArchiveQueueService.test.ts b/fluxer_api/src/api/infrastructure/KVThreadAutoArchiveQueueService.test.ts index 0e19be36b..41f7beb4f 100644 --- a/fluxer_api/src/api/infrastructure/KVThreadAutoArchiveQueueService.test.ts +++ b/fluxer_api/src/api/infrastructure/KVThreadAutoArchiveQueueService.test.ts @@ -50,6 +50,7 @@ function threadRow(threadId: ChannelID): ChannelRow { user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/instance/InstanceConfigRepository.ts b/fluxer_api/src/api/instance/InstanceConfigRepository.ts index 28c948fdb..093be64a8 100644 --- a/fluxer_api/src/api/instance/InstanceConfigRepository.ts +++ b/fluxer_api/src/api/instance/InstanceConfigRepository.ts @@ -88,6 +88,7 @@ import { PushRelayConfigSchema, toLegacyPushServiceDeliveryWire, } from '@fluxer/schema/src/domains/admin/PushRelaySchemas'; +import {type VoiceP2pConfig, VoiceP2pConfigSchema} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; import { type ExperimentDeliveryConfig, ExperimentDeliveryConfigSchema, @@ -109,6 +110,7 @@ import {z} from 'zod'; const GATEWAY_ROLLOUT_CONFIG_KEY = 'gateway_rollout_config'; const PUSH_RELAY_CONFIG_KEY = 'push_service_delivery_config'; const DOMAIN_MIGRATION_CONFIG_KEY = 'domain_migration_config'; +const VOICE_P2P_CONFIG_KEY = 'voice_p2p_config'; const CAPTCHA_CONFIG_KEY = 'captcha_config'; const CHANNEL_THREADS_CONFIG_KEY = 'channel_threads_config'; const EXPERIMENT_DELIVERY_CONFIG_KEY = 'experiment_delivery_config'; @@ -442,6 +444,7 @@ type StoredConfigSection = | 'gateway rollout' | 'push relay' | 'domain migration' + | 'voice p2p' | 'captcha' | 'channel threads' | 'experiment delivery' @@ -661,6 +664,10 @@ function parseStoredDomainMigrationConfig(raw: string | null): DomainMigrationCo return parseStoredConfigOrDefault(DomainMigrationConfigSchema, raw, 'domain migration'); } +function parseStoredVoiceP2pConfig(raw: string | null): VoiceP2pConfig { + return parseStoredConfigOrDefault(VoiceP2pConfigSchema, raw, 'voice p2p'); +} + function parseStoredCaptchaConfig(raw: string | null): CaptchaConfig { return parseStoredConfigOrDefault(CaptchaConfigSchema, raw, 'captcha'); } @@ -1350,6 +1357,7 @@ export class InstanceConfigRepository { ); parseStoredPushRelayConfig(snapshot.get(PUSH_RELAY_CONFIG_KEY) ?? null); parseStoredDomainMigrationConfig(snapshot.get(DOMAIN_MIGRATION_CONFIG_KEY) ?? null); + parseStoredVoiceP2pConfig(snapshot.get(VOICE_P2P_CONFIG_KEY) ?? null); parseStoredCaptchaConfig(snapshot.get(CAPTCHA_CONFIG_KEY) ?? null); syncChannelThreadsConfig(snapshot.get(CHANNEL_THREADS_CONFIG_KEY) ?? null, parseStoredChannelThreadsConfig); parseStoredExperimentDeliveryConfig(snapshot.get(EXPERIMENT_DELIVERY_CONFIG_KEY) ?? null); @@ -1595,6 +1603,21 @@ export class InstanceConfigRepository { ); } + async getVoiceP2pConfig(): Promise { + const raw = await this.getConfig(VOICE_P2P_CONFIG_KEY); + return parseStoredVoiceP2pConfig(raw); + } + + async setVoiceP2pConfig(config: VoiceP2pConfig): Promise { + await this.updateVoiceP2pConfig(() => config); + } + + updateVoiceP2pConfig(update: (current: VoiceP2pConfig) => VoiceP2pConfig): Promise { + return this.updateStoredConfig(VOICE_P2P_CONFIG_KEY, (raw) => + validateStoredConfig(VoiceP2pConfigSchema, update(parseStoredVoiceP2pConfig(raw)), 'voice p2p'), + ); + } + async getCaptchaConfig(): Promise { const raw = await this.getConfig(CAPTCHA_CONFIG_KEY); return parseStoredCaptchaConfig(raw); diff --git a/fluxer_api/src/api/middleware/ServiceMiddleware.ts b/fluxer_api/src/api/middleware/ServiceMiddleware.ts index 195a91538..ad6a5b628 100644 --- a/fluxer_api/src/api/middleware/ServiceMiddleware.ts +++ b/fluxer_api/src/api/middleware/ServiceMiddleware.ts @@ -298,6 +298,7 @@ class RequestServices implements RequestScopedServices { getChannelRepository(), voiceRoomStore, voiceAvailabilityService, + getInstanceConfigRepository(), ) : null; } diff --git a/fluxer_api/src/api/middleware/ServiceSingletons.ts b/fluxer_api/src/api/middleware/ServiceSingletons.ts index 2be0b6bfd..d8eec3525 100644 --- a/fluxer_api/src/api/middleware/ServiceSingletons.ts +++ b/fluxer_api/src/api/middleware/ServiceSingletons.ts @@ -90,6 +90,7 @@ import {UserActivityBuffer} from '@app/api/user/services/UserActivityBuffer'; import {UserContactChangeLogService} from '@app/api/user/services/UserContactChangeLogService'; import {awaitAll} from '@app/api/utils/ConcurrencyUtils'; import {UserPermissionUtils} from '@app/api/utils/UserPermissionUtils'; +import {VoiceP2pConnectionReportRepository} from '@app/api/voice/VoiceP2pConnectionReportRepository'; import {VoiceRepository} from '@app/api/voice/VoiceRepository'; import {SweegoWebhookService} from '@app/api/webhook/SweegoWebhookService'; import {WebhookRepository} from '@app/api/webhook/WebhookRepository'; @@ -114,6 +115,7 @@ export const getReadStateRepository = singleton(() => new ReadStateRepository()) export const getFavoriteMemeRepository = singleton(() => new FavoriteMemeRepository()); export const getConnectionRepository = singleton(() => new ConnectionRepository()); export const getReportRepository = singleton(() => new ReportRepository()); +export const getVoiceP2pConnectionReportRepository = singleton(() => new VoiceP2pConnectionReportRepository()); export const getAdminRepository = singleton(() => new AdminRepository()); export const getAdminArchiveRepository = singleton(() => new AdminArchiveRepository()); export const getVoiceRepository = singleton(() => new VoiceRepository()); diff --git a/fluxer_api/src/api/models/Channel.ts b/fluxer_api/src/api/models/Channel.ts index 257893f13..1c50447dc 100644 --- a/fluxer_api/src/api/models/Channel.ts +++ b/fluxer_api/src/api/models/Channel.ts @@ -32,6 +32,7 @@ export class Channel { readonly userLimit: number | null; readonly voiceConnectionLimit: number | null; readonly rtcRegion: string | null; + readonly rtcP2p: boolean; readonly lastMessageId: MessageID | null; readonly lastPinTimestamp: Date | null; readonly permissionOverwrites: Map; @@ -63,6 +64,7 @@ export class Channel { row.voice_connection_limit ?? (this.type === ChannelTypes.GUILD_VOICE ? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT : null); this.rtcRegion = row.rtc_region ?? null; + this.rtcP2p = row.rtc_p2p ?? false; this.lastMessageId = row.last_message_id ?? null; this.lastPinTimestamp = row.last_pin_timestamp ?? null; this.permissionOverwrites = new Map(); @@ -123,6 +125,7 @@ export class Channel { user_limit: this.userLimit, voice_connection_limit: this.voiceConnectionLimit, rtc_region: this.rtcRegion, + rtc_p2p: this.rtcP2p, last_message_id: this.lastMessageId, last_pin_timestamp: this.lastPinTimestamp, permission_overwrites: permOverwritesMap, diff --git a/fluxer_api/src/api/openapi/openapi.json b/fluxer_api/src/api/openapi/openapi.json index 5626929d6..0d219a64d 100644 --- a/fluxer_api/src/api/openapi/openapi.json +++ b/fluxer_api/src/api/openapi/openapi.json @@ -22247,6 +22247,60 @@ } } }, + "/voice/p2p/connection-reports": { + "post": { + "operationId": "create_voice_p2p_connection_reports", + "summary": "Report peer-to-peer connection outcomes", + "tags": ["Voice"], + "responses": { + "204": {"description": "No Content"}, + "400": { + "description": "Bad Request - The request was malformed or contained invalid data", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} + }, + "401": { + "description": "Unauthorized - Authentication is required or the token is invalid", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} + }, + "403": { + "description": "Forbidden - You do not have permission to perform this action", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} + }, + "429": { + "description": "Too Many Requests - You are being rate limited", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThrottledError"}}}, + "headers": { + "Retry-After": { + "description": "Number of seconds to wait before retrying (only on 429)", + "schema": {"type": "integer"} + }, + "X-RateLimit-Limit": { + "description": "The number of requests that can be made in the current window", + "schema": {"type": "integer"} + }, + "X-RateLimit-Remaining": { + "description": "The number of remaining requests that can be made", + "schema": {"type": "integer"} + }, + "X-RateLimit-Reset": { + "description": "Unix timestamp when the rate limit resets", + "schema": {"type": "integer"} + } + } + }, + "500": { + "description": "Internal Server Error - An unexpected error occurred", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} + } + }, + "description": "Records how peer connections of a peer-to-peer voice call settled, one report for each remote peer. Fluxer adds the request address, its country and the client platform of the session.", + "security": [{"sessionToken": []}], + "requestBody": { + "required": true, + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/VoiceP2pConnectionReportsRequest"}}} + } + } + }, "/webhooks/{webhook_id}": { "get": { "operationId": "get_webhook", @@ -24442,6 +24496,123 @@ "required": ["id", "guild_id", "channel_id", "name", "type", "user"], "additionalProperties": false }, + "VoiceP2pConnectionReportsRequest": { + "type": "object", + "properties": { + "reports": { + "minItems": 1, + "maxItems": 8, + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "description": "The voice channel or private channel of the call", + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "anyOf": [{"$ref": "#/components/schemas/SnowflakeType"}, {"type": "null"}], + "description": "The guild of the voice channel, null for a call" + }, + "participant_count": { + "type": "integer", + "minimum": 2, + "maximum": 4, + "description": "Participants in the call when the peer connection settled" + }, + "outcome": { + "description": "How the peer connection settled", + "x-enumNames": ["connected", "failed"], + "x-enumDescriptions": ["The peer connection reached connected", "The peer connection failed"], + "enum": ["connected", "failed"], + "type": "string" + }, + "local_candidate_type": { + "anyOf": [ + { + "description": "ICE candidate type of one end of the selected candidate pair", + "x-enumNames": ["host", "srflx", "prflx", "relay"], + "x-enumDescriptions": [ + "A local interface address", + "A public address a STUN server reflected", + "A public address learned from a peer during checks", + "A relayed address" + ], + "enum": ["host", "srflx", "prflx", "relay"], + "type": "string" + }, + {"type": "null"} + ], + "description": "Local candidate type, null without a selected pair" + }, + "remote_candidate_type": { + "anyOf": [ + { + "description": "ICE candidate type of one end of the selected candidate pair", + "x-enumNames": ["host", "srflx", "prflx", "relay"], + "x-enumDescriptions": [ + "A local interface address", + "A public address a STUN server reflected", + "A public address learned from a peer during checks", + "A relayed address" + ], + "enum": ["host", "srflx", "prflx", "relay"], + "type": "string" + }, + {"type": "null"} + ], + "description": "Remote candidate type, null without a selected pair" + }, + "ip_family": { + "anyOf": [ + { + "description": "Address family of the selected pair", + "x-enumNames": ["ipv4", "ipv6"], + "x-enumDescriptions": ["IPv4", "IPv6"], + "enum": ["ipv4", "ipv6"], + "type": "string" + }, + {"type": "null"} + ], + "description": "Address family of the selected pair, null without one" + }, + "protocol": { + "anyOf": [ + { + "description": "Transport protocol of the selected pair", + "x-enumNames": ["udp", "tcp"], + "x-enumDescriptions": ["UDP", "TCP"], + "enum": ["udp", "tcp"], + "type": "string" + }, + {"type": "null"} + ], + "description": "Transport protocol of the selected pair, null without one" + }, + "setup_ms": { + "anyOf": [{"$ref": "#/components/schemas/Int32Type"}, {"type": "null"}], + "description": "Milliseconds from peer connection creation to first connected" + }, + "ice_restarted": {"type": "boolean", "description": "Whether the peer connection ran an ICE restart"} + }, + "required": [ + "channel_id", + "guild_id", + "participant_count", + "outcome", + "local_candidate_type", + "remote_candidate_type", + "ip_family", + "protocol", + "setup_ms", + "ice_restarted" + ] + }, + "description": "One report for each remote peer that settled" + } + }, + "required": ["reports"] + }, "EntranceSoundPlayRequest": { "type": "object", "properties": {"sound_id": {"$ref": "#/components/schemas/SnowflakeType"}}, @@ -27199,6 +27370,10 @@ "anyOf": [{"$ref": "#/components/schemas/Int32Type"}, {"type": "null"}] }, "rtc_region": {"description": "The voice region ID for the voice channel", "type": ["string", "null"]}, + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" + }, "last_message_id": { "description": "The ID of the last message sent in this channel", "anyOf": [{"$ref": "#/components/schemas/SnowflakeStringType"}, {"type": "null"}] @@ -30683,7 +30858,8 @@ "properties": { "domain_migration": {"$ref": "#/components/schemas/DomainMigrationAssignmentResponse"}, "channel_threads": {"$ref": "#/components/schemas/ChannelThreadsAssignmentResponse"}, - "plutonium_page": {"$ref": "#/components/schemas/PlutoniumPageAssignmentResponse"} + "plutonium_page": {"$ref": "#/components/schemas/PlutoniumPageAssignmentResponse"}, + "voice_p2p": {"$ref": "#/components/schemas/VoiceP2pAssignmentResponse"} }, "additionalProperties": false } @@ -31005,6 +31181,10 @@ "anyOf": [{"$ref": "#/components/schemas/Int32Type"}, {"type": "null"}] }, "rtc_region": {"description": "The voice region ID for the voice channel", "type": ["string", "null"]}, + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" + }, "last_message_id": { "description": "The ID of the last message sent in this channel", "anyOf": [{"$ref": "#/components/schemas/SnowflakeStringType"}, {"type": "null"}] @@ -31356,6 +31536,10 @@ "anyOf": [{"$ref": "#/components/schemas/Int32Type"}, {"type": "null"}] }, "rtc_region": {"description": "The voice region ID for the voice channel", "type": ["string", "null"]}, + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" + }, "last_message_id": { "description": "The ID of the last message sent in this channel", "anyOf": [{"$ref": "#/components/schemas/SnowflakeStringType"}, {"type": "null"}] @@ -31946,97 +32130,9 @@ "description": "Voice region ID for the voice channel (1-64 characters)", "type": ["string", "null"] }, - "name": {"description": "The name of the channel", "type": ["string", "null"]}, - "type": { - "description": "Convert between text (0) and announcement (5). Other conversions are rejected.", - "anyOf": [{"type": "number", "const": 0}, {"type": "number", "const": 5}] - } - } - }, - { - "type": "object", - "properties": { - "topic": {"description": "The channel topic (1-1024 characters)", "type": ["string", "null"]}, - "url": {"description": "External URL for link channels", "type": ["string", "null"]}, - "parent_id": { - "description": "ID of the parent category for this channel", - "anyOf": [{"$ref": "#/components/schemas/SnowflakeType"}, {"type": "null"}] - }, - "bitrate": { - "description": "Voice channel bitrate in bits per second (8000-384000), clamped to 96000 unless the guild holds an AUDIO_BITRATE feature", - "anyOf": [{"type": "integer", "minimum": 8000, "maximum": 384000}, {"type": "null"}] - }, - "user_limit": { - "description": "Maximum users allowed in voice channel (0-99, 0 means unlimited)", - "anyOf": [{"type": "integer", "minimum": 0, "maximum": 99}, {"type": "null"}] - }, - "voice_connection_limit": { - "description": "Maximum active voice connections allowed per user in a voice channel (1-100)", - "anyOf": [{"type": "integer", "minimum": 1, "maximum": 100}, {"type": "null"}] - }, - "permission_overwrites": { - "description": "Permission overwrites for roles and members", - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "description": "The ID of the role or user to overwrite permissions for", - "$ref": "#/components/schemas/SnowflakeType" - }, - "type": { - "description": "The type of overwrite (0 = role, 1 = member)", - "x-enumNames": ["ROLE", "MEMBER"], - "enum": [0, 1], - "type": "integer" - }, - "allow": { - "description": "Bitwise value of allowed permissions", - "$ref": "#/components/schemas/UnsignedInt64Type" - }, - "deny": { - "description": "Bitwise value of denied permissions", - "$ref": "#/components/schemas/UnsignedInt64Type" - } - }, - "required": ["id", "type"] - } - }, - "rate_limit_per_user": { - "description": "Slowmode delay in seconds (0-21600)", - "anyOf": [{"type": "integer", "minimum": 0, "maximum": 21600}, {"type": "null"}] - }, - "nsfw": { - "description": "Legacy: setting true maps to nsfw_override=true; setting false maps to nsfw_override=null (inherit). Prefer nsfw_override.", - "type": ["boolean", "null"] - }, - "nsfw_override": { - "description": "Per-channel adult-content override (true=on, false=off, null=inherit from category then guild). Takes precedence over the legacy `nsfw` field if both are present.", - "type": ["boolean", "null"] - }, - "content_warning_level": { - "description": "Channel-level content warning override (0=inherit, 1=force-warn)", - "$ref": "#/components/schemas/ContentWarningLevelInput" - }, - "content_warning_text": { - "description": "Custom channel content warning text (max 200 characters); null inherits from parent or guild", - "anyOf": [{"type": "string", "maxLength": 200}, {"type": "null"}] - }, - "icon": { - "description": "Base64-encoded icon image for group DM channels", - "anyOf": [{"$ref": "#/components/schemas/Base64ImageType"}, {"type": "null"}] - }, - "owner_id": { - "description": "ID of the new owner for group DM channels", - "anyOf": [{"$ref": "#/components/schemas/SnowflakeType"}, {"type": "null"}] - }, - "nicks": { - "description": "Custom nicknames for users in this channel", - "$ref": "#/components/schemas/ChannelNicknameOverrides" - }, - "rtc_region": { - "description": "Voice region ID for the voice channel (1-64 characters)", - "type": ["string", "null"] + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" }, "name": {"description": "The name of the channel", "type": ["string", "null"]}, "type": { @@ -32130,6 +32226,106 @@ "description": "Voice region ID for the voice channel (1-64 characters)", "type": ["string", "null"] }, + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" + }, + "name": {"description": "The name of the channel", "type": ["string", "null"]}, + "type": { + "description": "Convert between text (0) and announcement (5). Other conversions are rejected.", + "anyOf": [{"type": "number", "const": 0}, {"type": "number", "const": 5}] + } + } + }, + { + "type": "object", + "properties": { + "topic": {"description": "The channel topic (1-1024 characters)", "type": ["string", "null"]}, + "url": {"description": "External URL for link channels", "type": ["string", "null"]}, + "parent_id": { + "description": "ID of the parent category for this channel", + "anyOf": [{"$ref": "#/components/schemas/SnowflakeType"}, {"type": "null"}] + }, + "bitrate": { + "description": "Voice channel bitrate in bits per second (8000-384000), clamped to 96000 unless the guild holds an AUDIO_BITRATE feature", + "anyOf": [{"type": "integer", "minimum": 8000, "maximum": 384000}, {"type": "null"}] + }, + "user_limit": { + "description": "Maximum users allowed in voice channel (0-99, 0 means unlimited)", + "anyOf": [{"type": "integer", "minimum": 0, "maximum": 99}, {"type": "null"}] + }, + "voice_connection_limit": { + "description": "Maximum active voice connections allowed per user in a voice channel (1-100)", + "anyOf": [{"type": "integer", "minimum": 1, "maximum": 100}, {"type": "null"}] + }, + "permission_overwrites": { + "description": "Permission overwrites for roles and members", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "description": "The ID of the role or user to overwrite permissions for", + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "description": "The type of overwrite (0 = role, 1 = member)", + "x-enumNames": ["ROLE", "MEMBER"], + "enum": [0, 1], + "type": "integer" + }, + "allow": { + "description": "Bitwise value of allowed permissions", + "$ref": "#/components/schemas/UnsignedInt64Type" + }, + "deny": { + "description": "Bitwise value of denied permissions", + "$ref": "#/components/schemas/UnsignedInt64Type" + } + }, + "required": ["id", "type"] + } + }, + "rate_limit_per_user": { + "description": "Slowmode delay in seconds (0-21600)", + "anyOf": [{"type": "integer", "minimum": 0, "maximum": 21600}, {"type": "null"}] + }, + "nsfw": { + "description": "Legacy: setting true maps to nsfw_override=true; setting false maps to nsfw_override=null (inherit). Prefer nsfw_override.", + "type": ["boolean", "null"] + }, + "nsfw_override": { + "description": "Per-channel adult-content override (true=on, false=off, null=inherit from category then guild). Takes precedence over the legacy `nsfw` field if both are present.", + "type": ["boolean", "null"] + }, + "content_warning_level": { + "description": "Channel-level content warning override (0=inherit, 1=force-warn)", + "$ref": "#/components/schemas/ContentWarningLevelInput" + }, + "content_warning_text": { + "description": "Custom channel content warning text (max 200 characters); null inherits from parent or guild", + "anyOf": [{"type": "string", "maxLength": 200}, {"type": "null"}] + }, + "icon": { + "description": "Base64-encoded icon image for group DM channels", + "anyOf": [{"$ref": "#/components/schemas/Base64ImageType"}, {"type": "null"}] + }, + "owner_id": { + "description": "ID of the new owner for group DM channels", + "anyOf": [{"$ref": "#/components/schemas/SnowflakeType"}, {"type": "null"}] + }, + "nicks": { + "description": "Custom nicknames for users in this channel", + "$ref": "#/components/schemas/ChannelNicknameOverrides" + }, + "rtc_region": { + "description": "Voice region ID for the voice channel (1-64 characters)", + "type": ["string", "null"] + }, + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" + }, "name": {"description": "The name of the channel", "type": ["string", "null"]} } }, @@ -32218,6 +32414,10 @@ "description": "Voice region ID for the voice channel (1-64 characters)", "type": ["string", "null"] }, + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" + }, "name": {"description": "The name of the category", "type": ["string", "null"]} } }, @@ -32306,6 +32506,10 @@ "description": "Voice region ID for the voice channel (1-64 characters)", "type": ["string", "null"] }, + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" + }, "name": {"description": "The name of the channel", "type": ["string", "null"]} } }, @@ -34009,6 +34213,10 @@ "description": "Voice region ID for the voice channel (1-64 characters)", "type": ["string", "null"] }, + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" + }, "name": {"description": "The name of the channel", "type": ["string", "null"]}, "default_auto_archive_duration": { "description": "Default auto archive duration in minutes for new threads", @@ -34118,6 +34326,10 @@ "description": "Voice region ID for the voice channel (1-64 characters)", "type": ["string", "null"] }, + "rtc_p2p": { + "description": "Whether calls in the voice channel connect participants directly instead of through a voice server", + "type": "boolean" + }, "name": {"description": "The name of the channel", "type": ["string", "null"]}, "default_auto_archive_duration": { "description": "Default auto archive duration in minutes for new threads", @@ -36155,6 +36367,15 @@ "additionalProperties": false }, "DonationCurrency": {"type": "string", "enum": ["usd", "eur", "brl", "inr", "pln", "try", "sek", "dkk", "nok"]}, + "VoiceP2pAssignmentResponse": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "max_participants": {"type": "integer", "minimum": 2, "maximum": 4} + }, + "required": ["enabled", "max_participants"], + "additionalProperties": false + }, "PlutoniumPageAssignmentResponse": { "type": "object", "properties": {"enabled": {"type": "boolean"}}, diff --git a/fluxer_api/src/api/rate_limit_configs/ChannelRateLimitConfig.ts b/fluxer_api/src/api/rate_limit_configs/ChannelRateLimitConfig.ts index 794391d75..2c85d2afe 100644 --- a/fluxer_api/src/api/rate_limit_configs/ChannelRateLimitConfig.ts +++ b/fluxer_api/src/api/rate_limit_configs/ChannelRateLimitConfig.ts @@ -180,4 +180,8 @@ export const ChannelRateLimitConfigs = { bucket: 'voice:entrance_sound:play::user_id::channel_id', config: {limit: 3, windowMs: ms('30 seconds')}, } as RouteRateLimitConfig, + VOICE_P2P_CONNECTION_REPORTS: { + bucket: 'voice:p2p:connection_reports', + config: {limit: 10, windowMs: ms('1 minute')}, + } as RouteRateLimitConfig, } as const; diff --git a/fluxer_api/src/api/rpc/RpcService.ts b/fluxer_api/src/api/rpc/RpcService.ts index daac24e78..8e00ac3fb 100644 --- a/fluxer_api/src/api/rpc/RpcService.ts +++ b/fluxer_api/src/api/rpc/RpcService.ts @@ -501,6 +501,10 @@ export class RpcService { canStream: request.can_stream, canVideo: request.can_video, tokenNonce: request.token_nonce, + p2p: request.p2p, + p2pInitiator: request.p2p_initiator, + p2pParticipantCount: request.p2p_participant_count, + countryCode: request.country_code, }); return { type: 'voice_get_token', @@ -1286,6 +1290,7 @@ export class RpcService { ), pinned_dms: timeRpcStepSync(responseBuildSteps, 'map_pinned_dms', () => userData.pinnedDMs?.map(String) ?? []), country_code: countryCode, + geoip_country_code: geoipCountryIso, latitude: geoipLatitude, longitude: geoipLongitude, rtc_regions: rtcRegions, diff --git a/fluxer_api/src/api/rpc/tests/RpcGuildVoiceStateP2p.test.ts b/fluxer_api/src/api/rpc/tests/RpcGuildVoiceStateP2p.test.ts new file mode 100644 index 000000000..7121d8a32 --- /dev/null +++ b/fluxer_api/src/api/rpc/tests/RpcGuildVoiceStateP2p.test.ts @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; +import {createChannel, createGuild} from '@app/api/guild/tests/GuildTestUtils'; +import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; +import {HTTP_STATUS} from '@app/api/test/TestConstants'; +import {createBuilder} from '@app/api/test/TestRequestBuilder'; +import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; +import {afterEach, beforeEach, describe, expect, test} from 'vitest'; + +interface RpcVoiceStatesResponse { + type: 'guild_collection'; + data: { + voice_states: Array<{connection_id: string; p2p?: boolean}>; + }; +} + +describe('RpcService guild voice state p2p flag', () => { + let harness: ApiTestHarness; + beforeEach(async () => { + harness = await createApiTestHarness(); + }); + afterEach(async () => { + await harness?.shutdown(); + }); + test('keeps p2p on a persisted guild voice state', async () => { + const account = await createTestAccount(harness); + const guild = await createGuild(harness, account.token, 'P2P Voice State Guild'); + const channel = await createChannel(harness, account.token, guild.id, 'voice', ChannelTypes.GUILD_VOICE); + await createBuilder(harness, '') + .post('/test/rpc-session-init') + .body({ + type: 'voice_state_upsert', + guild_id: guild.id, + voice_state: { + guild_id: guild.id, + channel_id: channel.id, + user_id: account.userId, + connection_id: 'brisk-forte', + mute: false, + deaf: false, + self_mute: false, + self_deaf: false, + p2p: true, + }, + }) + .expect(HTTP_STATUS.OK) + .execute(); + const response = await createBuilder(harness, '') + .post('/test/rpc-session-init') + .body({type: 'guild_collection', guild_id: guild.id, collection: 'voice_states'}) + .expect(HTTP_STATUS.OK) + .execute(); + expect(response.data.voice_states).toMatchObject([{connection_id: 'brisk-forte', p2p: true}]); + }); +}); diff --git a/fluxer_api/src/api/rpc/tests/RpcSessionGeoipCountry.test.ts b/fluxer_api/src/api/rpc/tests/RpcSessionGeoipCountry.test.ts new file mode 100644 index 000000000..87454601c --- /dev/null +++ b/fluxer_api/src/api/rpc/tests/RpcSessionGeoipCountry.test.ts @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; +import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; +import {HTTP_STATUS} from '@app/api/test/TestConstants'; +import {createBuilder} from '@app/api/test/TestRequestBuilder'; +import {afterEach, beforeEach, describe, expect, test} from 'vitest'; + +interface RpcSessionResponse { + type: 'session'; + data: { + country_code: string; + geoip_country_code: string | null; + }; +} + +describe('RpcService session geoip country', () => { + let harness: ApiTestHarness; + beforeEach(async () => { + harness = await createApiTestHarness(); + }); + afterEach(async () => { + await harness?.shutdown(); + }); + test('reports an unknown country as null without the country_code default', async () => { + const account = await createTestAccount(harness); + const response = await createBuilder(harness, '') + .post('/test/rpc-session-init') + .body({type: 'session', token: account.token, version: 1, ip: '127.0.0.1'}) + .expect(HTTP_STATUS.OK) + .execute(); + expect(response.data.geoip_country_code).toBeNull(); + expect(response.data.country_code).toBe('US'); + }); +}); diff --git a/fluxer_api/src/api/test/TestHarnessController.ts b/fluxer_api/src/api/test/TestHarnessController.ts index bdf9f0d2b..4f098635b 100644 --- a/fluxer_api/src/api/test/TestHarnessController.ts +++ b/fluxer_api/src/api/test/TestHarnessController.ts @@ -989,6 +989,7 @@ export function TestHarnessController(app: HonoApp) { user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: lastMessageId, last_pin_timestamp: null, permission_overwrites: null, @@ -1031,6 +1032,7 @@ export function TestHarnessController(app: HonoApp) { user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: lastMessageId, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/user/repositories/UserChannelRepository.ts b/fluxer_api/src/api/user/repositories/UserChannelRepository.ts index 80892c433..b2f48c4b3 100644 --- a/fluxer_api/src/api/user/repositories/UserChannelRepository.ts +++ b/fluxer_api/src/api/user/repositories/UserChannelRepository.ts @@ -156,6 +156,7 @@ function buildDmChannelRow({ user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/user/services/UserChannelService.ts b/fluxer_api/src/api/user/services/UserChannelService.ts index 23f64d61f..b03a3ad01 100644 --- a/fluxer_api/src/api/user/services/UserChannelService.ts +++ b/fluxer_api/src/api/user/services/UserChannelService.ts @@ -522,6 +522,7 @@ export class UserChannelService { user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/user/tests/ListEndpointUserPartialPrefetch.test.ts b/fluxer_api/src/api/user/tests/ListEndpointUserPartialPrefetch.test.ts index acfd27ce7..5aed830ac 100644 --- a/fluxer_api/src/api/user/tests/ListEndpointUserPartialPrefetch.test.ts +++ b/fluxer_api/src/api/user/tests/ListEndpointUserPartialPrefetch.test.ts @@ -86,6 +86,7 @@ function createPrivateChannel(channelId: ChannelID, type: number, recipientIds: user_limit: null, voice_connection_limit: null, rtc_region: null, + rtc_p2p: null, last_message_id: null, last_pin_timestamp: null, permission_overwrites: null, diff --git a/fluxer_api/src/api/utils/AuditSerializationUtils.ts b/fluxer_api/src/api/utils/AuditSerializationUtils.ts index c64e0c303..ecae91bf9 100644 --- a/fluxer_api/src/api/utils/AuditSerializationUtils.ts +++ b/fluxer_api/src/api/utils/AuditSerializationUtils.ts @@ -61,6 +61,7 @@ export function serializeChannelForAudit(channel: Channel): Record { + const {reports} = ctx.req.valid('json'); + if (!(await ctx.get('instanceConfigRepository').getVoiceP2pConfig()).enabled) { + return ctx.body(null, 204); + } + const authSession = ctx.get('authSession'); + const {countryCode, normalizedIp} = await lookupGeoip(ctx.req.raw); + const clientInfo = resolveSessionClientInfo({ + userAgent: authSession?.clientUserAgent ?? null, + reportedOs: authSession?.clientOs ?? null, + productName: getInstanceProductName(), + }); + const userId = ctx.get('user').id; + const snowflakeService = ctx.get('snowflakeService'); + const reportedAt = new Date(); + const rows = await Promise.all( + reports.map(async (report) => ({ + user_id: userId, + report_id: await snowflakeService.generate(), + reported_at: reportedAt, + channel_id: createChannelID(report.channel_id), + guild_id: report.guild_id === null ? null : createGuildID(report.guild_id), + participant_count: report.participant_count, + outcome: report.outcome, + local_candidate_type: report.local_candidate_type, + remote_candidate_type: report.remote_candidate_type, + ip_family: report.ip_family, + protocol: report.protocol, + setup_ms: report.setup_ms, + ice_restarted: report.ice_restarted, + country: countryCode, + ip: normalizedIp, + client_platform: clientInfo.platform, + client_os: clientInfo.os, + })), + ); + await getVoiceP2pConnectionReportRepository().insertReports(rows); + return ctx.body(null, 204); + }, + ); +} diff --git a/fluxer_api/src/api/voice/VoiceP2pConnectionReportRepository.ts b/fluxer_api/src/api/voice/VoiceP2pConnectionReportRepository.ts new file mode 100644 index 000000000..e152cd57f --- /dev/null +++ b/fluxer_api/src/api/voice/VoiceP2pConnectionReportRepository.ts @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {BatchBuilder} from '@app/api/database/CassandraQueryExecution'; +import type {VoiceP2pConnectionReportRow} from '@app/api/database/types/VoiceTypes'; +import {VoiceP2pConnectionReports} from '@app/api/Tables'; + +export class VoiceP2pConnectionReportRepository { + async insertReports(rows: ReadonlyArray): Promise { + const batch = new BatchBuilder(); + for (const row of rows) { + batch.addPrepared(VoiceP2pConnectionReports.insert(row)); + } + await batch.execute(); + } +} diff --git a/fluxer_api/src/api/voice/VoiceService.ts b/fluxer_api/src/api/voice/VoiceService.ts index e689e237d..9ed78acf5 100644 --- a/fluxer_api/src/api/voice/VoiceService.ts +++ b/fluxer_api/src/api/voice/VoiceService.ts @@ -1,12 +1,16 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes'; +import {Config} from '@app/api/Config'; import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import {resolveExperimentTargeting} from '@app/api/experiment/ExperimentTargeting'; import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; import type {ListParticipantsResult} from '@app/api/infrastructure/ILiveKitService'; import type {LiveKitService} from '@app/api/infrastructure/LiveKitService'; import type {PinnedRoomServer, VoiceRoomStore} from '@app/api/infrastructure/VoiceRoomStore'; +import type {InstanceConfigRepository} from '@app/api/instance/InstanceConfigRepository'; import {Logger} from '@app/api/Logger'; +import type {User} from '@app/api/models/User'; import type {IUserRepository} from '@app/api/user/IUserRepository'; import type {VoiceAccessContext, VoiceAvailabilityService} from '@app/api/voice/VoiceAvailabilityService'; import type {VoiceRegionAvailability, VoiceServerRecord} from '@app/api/voice/VoiceModel'; @@ -23,6 +27,7 @@ import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownCha import {FeatureTemporarilyDisabledError} from '@fluxer/errors/src/domains/core/FeatureTemporarilyDisabledError'; import {UnknownGuildMemberError} from '@fluxer/errors/src/domains/guild/UnknownGuildMemberError'; import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError'; +import {resolveVoiceP2pAssignment} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; interface GetVoiceTokenParams { guildId?: GuildID; @@ -36,8 +41,35 @@ interface GetVoiceTokenParams { canStream?: boolean; canVideo?: boolean; tokenNonce?: string; + p2p?: boolean; + p2pInitiator?: boolean; + p2pParticipantCount?: number; + countryCode?: string; } +interface VoiceTokenGrant { + token: string; + endpoint: string; + connectionId: string; + tokenNonce: string; + regionId: string; + serverId: string; +} + +interface VoiceP2pGrant { + p2p: true; + connectionId: string; + iceServers: Array<{urls: Array}>; + maxParticipants: number; +} + +interface VoiceP2pDecline { + p2p: false; + declineReason: 'channel_full'; +} + +type VoiceP2pDecision = {kind: 'granted'; maxParticipants: number} | {kind: 'declined'} | {kind: 'channel_full'}; + interface VoicePermissions { canSpeak: boolean; canStream: boolean; @@ -64,16 +96,10 @@ export class VoiceService { private channelRepository: IChannelRepository, private voiceRoomStore: VoiceRoomStore, private voiceAvailabilityService: VoiceAvailabilityService, + private instanceConfigRepository: InstanceConfigRepository, ) {} - async getVoiceToken(params: GetVoiceTokenParams): Promise<{ - token: string; - endpoint: string; - connectionId: string; - tokenNonce: string; - regionId: string; - serverId: string; - }> { + async getVoiceToken(params: GetVoiceTokenParams): Promise { const {guildId, channelId, userId, connectionId: providedConnectionId} = params; const selectionKey = this.createVoiceRoutingSelectionKey(guildId, channelId); let connectionId = providedConnectionId; @@ -120,6 +146,19 @@ export class VoiceService { guildFeatures = guild.features; } } + const p2pDecision: VoiceP2pDecision = params.p2p ? await this.decideVoiceP2p(user, params) : {kind: 'declined'}; + if (p2pDecision.kind === 'channel_full') { + return {p2p: false, declineReason: 'channel_full'}; + } + if (p2pDecision.kind === 'granted') { + const stunUrls = Config.voice.p2pStunUrls; + return { + p2p: true, + connectionId: providedConnectionId || generateConnectionId(), + iceServers: stunUrls?.length ? [{urls: stunUrls}] : [], + maxParticipants: p2pDecision.maxParticipants, + }; + } const context: VoiceAccessContext = { requestingUserId: userId, guildId, @@ -257,6 +296,22 @@ export class VoiceService { return {token, endpoint, connectionId, tokenNonce, regionId, serverId}; } + private async decideVoiceP2p(user: User, params: GetVoiceTokenParams): Promise { + const config = await this.instanceConfigRepository.getVoiceP2pConfig(); + if (!config.enabled) { + return {kind: 'declined'}; + } + if (params.p2pParticipantCount !== undefined && params.p2pParticipantCount > config.max_participants) { + return {kind: 'channel_full'}; + } + const granted: VoiceP2pDecision = {kind: 'granted', maxParticipants: config.max_participants}; + if (!params.p2pInitiator) { + return granted; + } + const targeting = await resolveExperimentTargeting(user, params.countryCode ?? null, [config]); + return resolveVoiceP2pAssignment(config, user.id.toString(), targeting).enabled ? granted : {kind: 'declined'}; + } + private createVoiceRoutingSelectionKey(guildId: GuildID | undefined, channelId: ChannelID): string { const guildKey = guildId?.toString() ?? 'dm'; return `${guildKey}:${channelId.toString()}`; diff --git a/fluxer_api/src/api/voice/tests/VoiceP2pConnectionReports.test.ts b/fluxer_api/src/api/voice/tests/VoiceP2pConnectionReports.test.ts new file mode 100644 index 000000000..a0c7adbd2 --- /dev/null +++ b/fluxer_api/src/api/voice/tests/VoiceP2pConnectionReports.test.ts @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {createUniqueEmail, createUniqueUsername} from '@app/api/auth/tests/AuthTestUtils'; +import {createUserID} from '@app/api/BrandedTypes'; +import {createTestBotAccount} from '@app/api/bot/tests/BotTestUtils'; +import {fetchMany} from '@app/api/database/CassandraQueryExecution'; +import { + VOICE_P2P_CONNECTION_REPORT_COLUMNS, + type VoiceP2pConnectionReportRow, +} from '@app/api/database/types/VoiceTypes'; +import {getInstanceProductName} from '@app/api/instance/ProductName'; +import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons'; +import {VoiceP2pConnectionReports} from '@app/api/Tables'; +import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; +import {HTTP_STATUS, TEST_CREDENTIALS, TEST_USER_DATA} from '@app/api/test/TestConstants'; +import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; +import {DEFAULT_VOICE_P2P_CONFIG} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; +import type {GeoipResult} from '@pkgs/geoip/src/GeoipLookup'; +import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'; + +const {lookupGeoipMock} = vi.hoisted(() => ({ + lookupGeoipMock: vi.fn(), +})); + +vi.mock('@app/api/utils/IpUtils', async (importOriginal) => ({ + ...(await importOriginal()), + lookupGeoip: lookupGeoipMock, +})); + +const ENDPOINT = '/voice/p2p/connection-reports'; +const CLIENT_IP = '203.0.113.10'; +const ANDROID_USER_AGENT = 'Fluxer Android/1.4.0'; + +const LIST_REPORTS = VoiceP2pConnectionReports.select({where: VoiceP2pConnectionReports.where.eq('user_id')}); + +function geoipCountry(countryCode: string | null): GeoipResult { + return {countryCode, normalizedIp: CLIENT_IP, city: 'São Paulo', region: 'SP', countryName: 'Brazil'}; +} + +function connectedReport(overrides: Record = {}): Record { + return { + channel_id: '1189375284394692610', + guild_id: null, + participant_count: 2, + outcome: 'connected', + local_candidate_type: 'srflx', + remote_candidate_type: 'host', + ip_family: 'ipv6', + protocol: 'udp', + setup_ms: 840, + ice_restarted: false, + ...overrides, + }; +} + +async function registerAndroidAccount(harness: ApiTestHarness): Promise<{userId: string; token: string}> { + const registration = await createBuilder<{user_id: string; token: string}>(harness, '') + .post('/auth/register') + .header('user-agent', ANDROID_USER_AGENT) + .header('x-forwarded-for', CLIENT_IP) + .body({ + email: createUniqueEmail('p2p-report'), + username: createUniqueUsername('p2preport'), + global_name: TEST_USER_DATA.DEFAULT_GLOBAL_NAME, + password: TEST_CREDENTIALS.STRONG_PASSWORD, + date_of_birth: TEST_USER_DATA.DEFAULT_DATE_OF_BIRTH, + consent: true, + }) + .execute(); + return {userId: registration.user_id, token: registration.token}; +} + +async function listReports(userId: string): Promise> { + return fetchMany(LIST_REPORTS.bind({user_id: createUserID(BigInt(userId))})); +} + +describe('POST /voice/p2p/connection-reports', () => { + let harness: ApiTestHarness; + + beforeAll(async () => { + harness = await createApiTestHarness(); + }); + + beforeEach(async () => { + await harness.reset(); + lookupGeoipMock.mockReset(); + lookupGeoipMock.mockResolvedValue(geoipCountry('BR')); + await getInstanceConfigRepository().setVoiceP2pConfig({...DEFAULT_VOICE_P2P_CONFIG, enabled: true}); + }); + + afterAll(async () => { + await harness.shutdown(); + }); + + it('rejects an unauthenticated caller', async () => { + await createBuilderWithoutAuth(harness) + .post(ENDPOINT) + .body({reports: [connectedReport()]}) + .expect(HTTP_STATUS.UNAUTHORIZED) + .execute(); + }); + + it('rejects a bot', async () => { + const bot = await createTestBotAccount(harness); + await createBuilder(harness, `Bot ${bot.botToken}`) + .post(ENDPOINT) + .body({reports: [connectedReport()]}) + .expect(HTTP_STATUS.FORBIDDEN) + .execute(); + }); + + it('stores each report with the server-derived fields and the request address', async () => { + const account = await registerAndroidAccount(harness); + + await createBuilder(harness, account.token) + .post(ENDPOINT) + .header('x-forwarded-for', CLIENT_IP) + .body({ + reports: [ + connectedReport(), + connectedReport({ + guild_id: '1189375284394692608', + participant_count: 3, + outcome: 'failed', + local_candidate_type: null, + remote_candidate_type: null, + ip_family: null, + protocol: null, + setup_ms: null, + ice_restarted: true, + }), + ], + }) + .expect(HTTP_STATUS.NO_CONTENT) + .execute(); + + const rows = await listReports(account.userId); + expect(rows).toHaveLength(2); + const shared = { + user_id: BigInt(account.userId), + channel_id: 1189375284394692610n, + country: 'BR', + client_platform: `${getInstanceProductName()} Android`, + client_os: 'Android', + }; + const connected = rows.find((row) => row.outcome === 'connected'); + const failed = rows.find((row) => row.outcome === 'failed'); + expect(connected).toMatchObject({ + ...shared, + guild_id: null, + participant_count: 2, + local_candidate_type: 'srflx', + remote_candidate_type: 'host', + ip_family: 'ipv6', + protocol: 'udp', + setup_ms: 840, + ice_restarted: false, + }); + expect(failed).toMatchObject({ + ...shared, + guild_id: 1189375284394692608n, + participant_count: 3, + local_candidate_type: null, + remote_candidate_type: null, + ip_family: null, + protocol: null, + setup_ms: null, + ice_restarted: true, + }); + for (const row of rows) { + expect(Object.keys(row).sort()).toEqual([...VOICE_P2P_CONNECTION_REPORT_COLUMNS].sort()); + expect(row.reported_at).toBeInstanceOf(Date); + expect(row.ip).toBe(CLIENT_IP); + } + expect(connected?.report_id).not.toEqual(failed?.report_id); + }); + + it('accepts and drops reports while the experiment is disabled', async () => { + await getInstanceConfigRepository().setVoiceP2pConfig(DEFAULT_VOICE_P2P_CONFIG); + const account = await registerAndroidAccount(harness); + + await createBuilder(harness, account.token) + .post(ENDPOINT) + .body({reports: [connectedReport()]}) + .expect(HTTP_STATUS.NO_CONTENT) + .execute(); + + expect(await listReports(account.userId)).toEqual([]); + }); + + it('stores a null country when the lookup finds none', async () => { + lookupGeoipMock.mockResolvedValue(geoipCountry(null)); + const account = await registerAndroidAccount(harness); + + await createBuilder(harness, account.token) + .post(ENDPOINT) + .body({reports: [connectedReport()]}) + .expect(HTTP_STATUS.NO_CONTENT) + .execute(); + + const rows = await listReports(account.userId); + expect(rows).toHaveLength(1); + expect(rows[0]?.country).toBeNull(); + }); + + it.each([ + {name: 'an empty batch', body: {reports: []}}, + {name: 'more than 8 reports', body: {reports: Array.from({length: 9}, () => connectedReport())}}, + {name: 'a single participant', body: {reports: [connectedReport({participant_count: 1})]}}, + {name: 'more than 4 participants', body: {reports: [connectedReport({participant_count: 5})]}}, + {name: 'an unknown outcome', body: {reports: [connectedReport({outcome: 'checking'})]}}, + {name: 'an unknown candidate type', body: {reports: [connectedReport({local_candidate_type: 'turn'})]}}, + {name: 'an unknown address family', body: {reports: [connectedReport({ip_family: 'ipx'})]}}, + {name: 'an unknown protocol', body: {reports: [connectedReport({protocol: 'sctp'})]}}, + {name: 'a negative setup time', body: {reports: [connectedReport({setup_ms: -1})]}}, + {name: 'a malformed channel id', body: {reports: [connectedReport({channel_id: 'abc'})]}}, + {name: 'a missing ice_restarted', body: {reports: [connectedReport({ice_restarted: undefined})]}}, + ])('rejects $name and stores nothing', async ({body}) => { + const account = await registerAndroidAccount(harness); + + await createBuilder(harness, account.token) + .post(ENDPOINT) + .body(body) + .expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_FORM_BODY') + .execute(); + + expect(await listReports(account.userId)).toEqual([]); + }); +}); diff --git a/fluxer_api/src/api/voice/tests/VoiceP2pGrant.test.ts b/fluxer_api/src/api/voice/tests/VoiceP2pGrant.test.ts new file mode 100644 index 000000000..87a41bc3d --- /dev/null +++ b/fluxer_api/src/api/voice/tests/VoiceP2pGrant.test.ts @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; +import { + type ChannelID, + createChannelID, + createGuildID, + createUserID, + type GuildID, + type UserID, +} from '@app/api/BrandedTypes'; +import {Config} from '@app/api/Config'; +import {createChannel, createGuild} from '@app/api/guild/tests/GuildTestUtils'; +import { + getChannelRepository, + getGuildRepository, + getInstanceConfigRepository, + getUserRepository, +} from '@app/api/middleware/ServiceSingletons'; +import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; +import {VoiceService} from '@app/api/voice/VoiceService'; +import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; +import {DEFAULT_VOICE_P2P_CONFIG, type VoiceP2pConfig} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; +import {afterEach, beforeEach, describe, expect, test} from 'vitest'; + +const SFU_PATH_REACHED = 'sfu path reached'; + +function createVoiceService(): VoiceService { + const sfuOnly = new Proxy( + {}, + { + get() { + throw new Error(SFU_PATH_REACHED); + }, + }, + ); + return new VoiceService( + sfuOnly as never, + getGuildRepository(), + getUserRepository(), + getChannelRepository(), + sfuOnly as never, + sfuOnly as never, + getInstanceConfigRepository(), + ); +} + +describe('VoiceService peer-to-peer grant', () => { + let harness: ApiTestHarness; + let voiceService: VoiceService; + let params: {guildId: GuildID; channelId: ChannelID; userId: UserID}; + beforeEach(async () => { + harness = await createApiTestHarness(); + voiceService = createVoiceService(); + const account = await createTestAccount(harness); + const guild = await createGuild(harness, account.token, 'P2P Guild'); + const channel = await createChannel(harness, account.token, guild.id, 'voice', ChannelTypes.GUILD_VOICE); + params = { + guildId: createGuildID(BigInt(guild.id)), + channelId: createChannelID(BigInt(channel.id)), + userId: createUserID(BigInt(account.userId)), + }; + }); + afterEach(async () => { + Config.voice.p2pStunUrls = undefined; + await harness?.shutdown(); + }); + + async function setConfig(config: Partial): Promise { + await getInstanceConfigRepository().setVoiceP2pConfig({...DEFAULT_VOICE_P2P_CONFIG, ...config}); + } + + test('grants an assigned initiator without selecting a voice server', async () => { + await setConfig({enabled: true, included_user_ids: [params.userId.toString()]}); + const grant = await voiceService.getVoiceToken({...params, p2p: true, p2pInitiator: true}); + expect(grant).toEqual({ + p2p: true, + connectionId: expect.stringMatching(/^[a-z]+-[a-z]+$/u), + iceServers: [], + maxParticipants: 2, + }); + }); + + test('hands out the configured STUN urls as one entry without credentials', async () => { + Config.voice.p2pStunUrls = ['stun:stun.example.com:3478', 'stun:stun2.example.com:3478']; + await setConfig({enabled: true}); + const grant = await voiceService.getVoiceToken({...params, p2p: true, connectionId: 'brisk-forte'}); + expect(grant).toEqual({ + p2p: true, + connectionId: 'brisk-forte', + iceServers: [{urls: ['stun:stun.example.com:3478', 'stun:stun2.example.com:3478']}], + maxParticipants: 2, + }); + }); + + test('carries the configured cap so the call can enforce it on join', async () => { + await setConfig({enabled: true, max_participants: 3}); + const grant = await voiceService.getVoiceToken({...params, p2p: true, p2pParticipantCount: 2}); + expect(grant).toMatchObject({p2p: true, maxParticipants: 3}); + }); + + test('declines a joiner past the configured cap as channel full', async () => { + await setConfig({enabled: true, max_participants: 3}); + const grant = await voiceService.getVoiceToken({...params, p2p: true, p2pParticipantCount: 3}); + expect(grant).toMatchObject({p2p: true}); + await expect(voiceService.getVoiceToken({...params, p2p: true, p2pParticipantCount: 4})).resolves.toEqual({ + p2p: false, + declineReason: 'channel_full', + }); + }); + + test('never refuses an initiator for size at the lowest cap', async () => { + await setConfig({enabled: true, max_participants: 2, included_user_ids: [params.userId.toString()]}); + const grant = await voiceService.getVoiceToken({ + ...params, + p2p: true, + p2pInitiator: true, + p2pParticipantCount: 1, + }); + expect(grant).toMatchObject({p2p: true}); + }); + + test('declines every request while disabled whatever the count', async () => { + await setConfig({enabled: false, max_participants: 2}); + await expect(voiceService.getVoiceToken({...params, p2p: true, p2pParticipantCount: 3})).rejects.toThrow( + SFU_PATH_REACHED, + ); + }); + + test('keeps the connection id the gateway supplies', async () => { + await setConfig({enabled: true}); + const grant = await voiceService.getVoiceToken({...params, p2p: true, connectionId: 'brisk-forte'}); + expect(grant).toEqual({p2p: true, connectionId: 'brisk-forte', iceServers: [], maxParticipants: 2}); + }); + + test('grants a joiner outside the rollout while the experiment is enabled', async () => { + await setConfig({enabled: true, excluded_user_ids: [params.userId.toString()]}); + const grant = await voiceService.getVoiceToken({...params, p2p: true, p2pInitiator: false}); + expect(grant).toMatchObject({p2p: true}); + }); + + test('declines an initiator outside the rollout', async () => { + await setConfig({enabled: true}); + await expect(voiceService.getVoiceToken({...params, p2p: true, p2pInitiator: true})).rejects.toThrow( + SFU_PATH_REACHED, + ); + }); + + test('applies the forwarded country to the initiator rollout', async () => { + await setConfig({enabled: true, rollout_basis_points: 10_000, rollout_country_codes: ['SE']}); + const grant = await voiceService.getVoiceToken({...params, p2p: true, p2pInitiator: true, countryCode: 'SE'}); + expect(grant).toMatchObject({p2p: true}); + await expect( + voiceService.getVoiceToken({...params, p2p: true, p2pInitiator: true, countryCode: 'US'}), + ).rejects.toThrow(SFU_PATH_REACHED); + await expect(voiceService.getVoiceToken({...params, p2p: true, p2pInitiator: true})).rejects.toThrow( + SFU_PATH_REACHED, + ); + }); + + test('declines every request while the experiment is disabled', async () => { + await setConfig({enabled: false, included_user_ids: [params.userId.toString()]}); + await expect(voiceService.getVoiceToken({...params, p2p: true, p2pInitiator: false})).rejects.toThrow( + SFU_PATH_REACHED, + ); + }); + + test('takes the ordinary path when the gateway does not ask for peer-to-peer', async () => { + await setConfig({enabled: true, included_user_ids: [params.userId.toString()]}); + await expect(voiceService.getVoiceToken(params)).rejects.toThrow(SFU_PATH_REACHED); + }); +}); diff --git a/fluxer_api/vitest.config.ts b/fluxer_api/vitest.config.ts index fd08fcf7f..33dcc6937 100644 --- a/fluxer_api/vitest.config.ts +++ b/fluxer_api/vitest.config.ts @@ -24,6 +24,7 @@ const configuredMaxConcurrency = parseParallelInteger(process.env.API_TEST_MAX_C const MODULE_REGISTRY_TEST_FILES = [ 'src/api/channel/tests/MessageCrosspostFanout.test.ts', + 'src/api/experiment/tests/ExperimentController.test.ts', 'src/api/gif/GifRequestCountry.test.ts', 'src/api/stripe/tests/StripeCheckoutCountryEnforcement.test.ts', 'src/api/stripe/tests/StripeNordicCurrencies.test.ts', @@ -33,6 +34,7 @@ const MODULE_REGISTRY_TEST_FILES = [ 'src/api/worker/tests/HandleMentionsThread.test.ts', 'src/api/search/tests/GuildSearchThreadScope.test.ts', 'src/api/worker/tasks/HarvestUserData.test.ts', + 'src/api/voice/tests/VoiceP2pConnectionReports.test.ts', ]; const INSTANCE_POLICY_TEST_FILES = [ diff --git a/fluxer_app/pkgs/livekit-client/VENDORING.md b/fluxer_app/pkgs/livekit-client/VENDORING.md index 93ba75db6..f2c857b53 100644 --- a/fluxer_app/pkgs/livekit-client/VENDORING.md +++ b/fluxer_app/pkgs/livekit-client/VENDORING.md @@ -18,6 +18,10 @@ Every file went through the same mechanical steps, so a fresh upstream tree can `connectionHelper`, `createLocalScreenTracks`, `getStereoAudioStreamTrack`, `isLocalPub`, the frame metadata worker, `room/token-source/test-tokens.ts`, `utils/subscribeToEvents.ts`, and the `DataTrackPacket`, data track packet extension and `LocalTrackRecorder` exports. `createV0RtcUrl`, `truncateBytes`, `videoQualityForRid` and `STOP_REFETCH_DELAY_MS` are no longer exported. +## Added seams + +`InternalRoomOptions.createEngine` lets a caller build the engine, and `Room` uses it in place of `new RTCEngine`. The index exports `RTCEngine`, `SignalClient`, `SignalConnectionState`, `PCTransportState`, `selectPublisherCodecPreferences` and the protocol messages a substitute engine has to build. The P2P mesh engine in `fluxer_app/src/features/voice/engine/mesh` depends on all of them. `room/Room.test.ts` covers the option. + ## Updating from upstream 1. Read the upstream changelog for the target version. diff --git a/fluxer_app/pkgs/livekit-client/src/index.ts b/fluxer_app/pkgs/livekit-client/src/index.ts index 06866a392..39d93393d 100644 --- a/fluxer_app/pkgs/livekit-client/src/index.ts +++ b/fluxer_app/pkgs/livekit-client/src/index.ts @@ -2,7 +2,20 @@ // // SPDX-License-Identifier: Apache-2.0 import {Mutex} from '@livekit/mutex'; -import {DataPacket_Kind, DisconnectReason, Encryption_Type, SubscriptionError, TrackType} from '@livekit/protocol'; +import { + Codec, + DataPacket_Kind, + DisconnectReason, + Encryption_Type, + JoinResponse, + ParticipantInfo, + ParticipantInfo_State, + ParticipantPermission, + SpeakerInfo, + SubscriptionError, + TrackInfo, + TrackType, +} from '@livekit/protocol'; import {getLogger, LoggerNames, LogLevel, setLogExtension, setLogLevel} from './logger.ts'; import * as attributes from './room/attribute-typings.ts'; import DefaultReconnectPolicy from './room/DefaultReconnectPolicy.ts'; @@ -60,6 +73,8 @@ import { } from './room/utils.ts'; import {getBrowser} from './utils/browserParser.ts'; +export type {AddTrackRequest} from '@livekit/protocol'; +export {SignalClient, SignalConnectionState, type SignalOptions} from './api/SignalClient.ts'; export type {BaseE2EEManager} from './e2ee/E2eeManager.ts'; export * from './e2ee/index.ts'; export { @@ -80,7 +95,9 @@ export type * from './room/data-stream/outgoing/StreamWriter.ts'; export type {DataTrackFrame} from './room/data-track/frame.ts'; export * from './room/errors.ts'; export * from './room/events.ts'; +export {type PCTransportManager, PCTransportState} from './room/PCTransportManager.ts'; export type {DataChannelKind} from './room/RTCEngine.ts'; +export {default as RTCEngine, selectPublisherCodecPreferences} from './room/RTCEngine.ts'; export {type PerformRpcParams, RpcError, type RpcInvocationData} from './room/rpc/index.ts'; export * from './room/token-source/TokenSource.ts'; export * from './room/token-source/types.ts'; @@ -127,6 +144,7 @@ export type { }; export { attributes, + Codec, ConnectionQuality, ConnectionState, CriticalTimers, @@ -150,6 +168,7 @@ export { isSVCCodec, isVideoCodec, isVideoTrack, + JoinResponse, LocalAudioTrack, LocalDataTrack, LocalParticipant, @@ -160,7 +179,10 @@ export { LogLevel, Mutex, Participant, + ParticipantInfo, + ParticipantInfo_State, ParticipantKind, + ParticipantPermission, RemoteAudioTrack, RemoteDataTrack, RemoteParticipant, @@ -168,6 +190,7 @@ export { RemoteTrackPublication, RemoteVideoTrack, Room, + SpeakerInfo, SubscriptionError, selectPreferredVideoCodec, setLogExtension, @@ -180,6 +203,7 @@ export { supportsScalabilityMode, supportsVideoCodec, supportsVP9, + TrackInfo, TrackPublication, TrackType, }; diff --git a/fluxer_app/pkgs/livekit-client/src/options.ts b/fluxer_app/pkgs/livekit-client/src/options.ts index b8d3f7479..49edc7113 100644 --- a/fluxer_app/pkgs/livekit-client/src/options.ts +++ b/fluxer_app/pkgs/livekit-client/src/options.ts @@ -4,6 +4,7 @@ import type {E2EEOptions} from './e2ee/types.ts'; import type {FrameMetadataOptions} from './frameMetadata/FrameMetadataManager.ts'; import type {ReconnectPolicy} from './room/ReconnectPolicy.ts'; +import type RTCEngine from './room/RTCEngine.ts'; import type { AudioCaptureOptions, AudioOutputOptions, @@ -54,6 +55,7 @@ export interface InternalRoomOptions { subscriberVideoCodecExclusions?: Array; h264HardwareProfiles?: ReadonlySet; dataStream?: RoomDataStreamOptions; + createEngine?: (options: InternalRoomOptions) => RTCEngine; } export interface RoomDataStreamOptions { maxPayloadByteLength?: number; diff --git a/fluxer_app/pkgs/livekit-client/src/room/Room.test.ts b/fluxer_app/pkgs/livekit-client/src/room/Room.test.ts new file mode 100644 index 000000000..94d9707a1 --- /dev/null +++ b/fluxer_app/pkgs/livekit-client/src/room/Room.test.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import {describe, expect, it, vi} from 'vitest'; +import type {InternalRoomOptions} from '../options.ts'; +import Room from './Room.ts'; +import RTCEngine from './RTCEngine.ts'; + +describe('Room createEngine option', () => { + it('builds the engine through the factory and hands it the merged room options', () => { + const createEngine = vi.fn((options: InternalRoomOptions) => new RTCEngine(options)); + const room = new Room({createEngine, dynacast: false}); + expect(createEngine).toHaveBeenCalledTimes(1); + expect(room.engine).toBe(createEngine.mock.results[0]?.value); + expect(createEngine.mock.calls[0]?.[0]).toBe(room.options); + }); + + it('builds a plain RTCEngine when no factory is set', () => { + const room = new Room({dynacast: false}); + expect(room.engine).toBeInstanceOf(RTCEngine); + }); +}); diff --git a/fluxer_app/pkgs/livekit-client/src/room/Room.ts b/fluxer_app/pkgs/livekit-client/src/room/Room.ts index 81c619ab4..5a2d74ae9 100644 --- a/fluxer_app/pkgs/livekit-client/src/room/Room.ts +++ b/fluxer_app/pkgs/livekit-client/src/room/Room.ts @@ -499,7 +499,7 @@ class Room extends (EventEmitter as new () => TypedEmitter) return; } - this.engine = new RTCEngine(this.options); + this.engine = this.options.createEngine ? this.options.createEngine(this.options) : new RTCEngine(this.options); this.engine.e2eeManager = this.e2eeManager; this.engine diff --git a/fluxer_app/src/features/app/constants/Endpoints.ts b/fluxer_app/src/features/app/constants/Endpoints.ts index 39fac9f43..f226edad6 100644 --- a/fluxer_app/src/features/app/constants/Endpoints.ts +++ b/fluxer_app/src/features/app/constants/Endpoints.ts @@ -233,6 +233,7 @@ export const Endpoints = { USER_ENTRANCE_SOUND: (soundId: string) => `/users/@me/entrance-sounds/${soundId}`, USER_ENTRANCE_SOUND_SELECTIONS: '/users/@me/entrance-sound-selections', VOICE_CHANNEL_ENTRANCE_SOUND: (channelId: string) => `/voice/channels/${channelId}/entrance-sound`, + VOICE_P2P_CONNECTION_REPORTS: '/voice/p2p/connection-reports', USER_FAVORITE_MEMES: (query = ME) => `/users/${query}/memes`, USER_FAVORITE_MEME: (query = ME, memeId: string) => `/users/${query}/memes/${memeId}`, USER_FAVORITE_GIFS_RESOLVE: (query = ME) => `/users/${query}/favorite-gifs/resolve`, diff --git a/fluxer_app/src/features/channel/commands/ChannelCommands.ts b/fluxer_app/src/features/channel/commands/ChannelCommands.ts index df5aa9684..949e59ada 100644 --- a/fluxer_app/src/features/channel/commands/ChannelCommands.ts +++ b/fluxer_app/src/features/channel/commands/ChannelCommands.ts @@ -46,6 +46,7 @@ type ChannelUpdateParams = Partial< | 'icon' | 'owner_id' | 'rtc_region' + | 'rtc_p2p' > >; diff --git a/fluxer_app/src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx b/fluxer_app/src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx index 33dcc6bf7..ea5087c67 100644 --- a/fluxer_app/src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx +++ b/fluxer_app/src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx @@ -44,6 +44,11 @@ import * as UnsavedChangesCommands from '@app/features/ui/commands/UnsavedChange import {Form} from '@app/features/ui/components/form/Form'; import {Input} from '@app/features/ui/components/form/FormInput'; import {Switch} from '@app/features/ui/components/form/FormSwitch'; +import VoiceP2pRollout from '@app/features/voice/state/VoiceP2pRollout'; +import { + CHANNEL_RTC_P2P_SETTING_DESCRIPTION_DESCRIPTOR, + CHANNEL_RTC_P2P_SETTING_DESCRIPTOR, +} from '@app/features/voice/utils/VoiceMessageDescriptors'; import {useRemoteFormReset} from '@app/lib/forms/RemoteFormReset'; import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import { @@ -154,6 +159,7 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId}) user_limit: 0, voice_connection_limit: VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT, rtc_region: null, + rtc_p2p: false, }, }); const threadDefaultsActive = @@ -181,6 +187,7 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId}) user_limit: channel.userLimit ?? 0, voice_connection_limit: channel.voiceConnectionLimit ?? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT, rtc_region: channel.rtcRegion ?? null, + rtc_p2p: channel.rtcP2p, } : null; useEffect(() => { @@ -257,6 +264,7 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId}) updateData.bitrate = Math.min(data.bitrate ?? BITRATE_KBPS_DEFAULT, maxBitrateKbps) * 1000; updateData.user_limit = data.user_limit; updateData.voice_connection_limit = data.voice_connection_limit ?? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT; + if (dirty.rtc_p2p) updateData.rtc_p2p = data.rtc_p2p; } else if (channel.type === ChannelTypes.GUILD_LINK) { updateData.url = data.url; } @@ -325,6 +333,7 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId}) default_thread_rate_limit_per_user: data.default_thread_rate_limit_per_user, } : {}), + rtc_p2p: data.rtc_p2p, }); ToastCommands.createToast({type: 'success', children: Channel updated}); }; @@ -493,6 +502,24 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId}) data-flx="channel.channel-tabs.channel-overview-tab.voice-settings" /> )} + {canManageChannel && VoiceP2pRollout.enabled && ( + ( + + )} + data-flx="channel.channel-tabs.channel-overview-tab.rtc-p2p-controller" + /> + )} {canUpdateRtcRegion && ( >; @@ -172,6 +173,7 @@ export class Channel { channel.voice_connection_limit ?? (this.type === ChannelTypes.GUILD_VOICE ? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT : null); this.rtcRegion = channel.rtc_region ?? null; + this.rtcP2p = channel.rtc_p2p ?? false; this.lastMessageId = channel.last_message_id ?? null; this.lastPinTimestamp = channel.last_pin_timestamp ? new Date(channel.last_pin_timestamp) : null; this.nsfw = channel.nsfw ?? false; @@ -368,6 +370,7 @@ export class Channel { voice_connection_limit: updates.voice_connection_limit !== undefined ? updates.voice_connection_limit : this.voiceConnectionLimit, rtc_region: updates.rtc_region !== undefined ? updates.rtc_region : this.rtcRegion, + rtc_p2p: updates.rtc_p2p ?? this.rtcP2p, last_message_id: updates.last_message_id !== undefined ? updates.last_message_id : this.lastMessageId, last_pin_timestamp: updates.last_pin_timestamp ?? this.lastPinTimestamp?.toISOString() ?? undefined, permission_overwrites: !this.isPrivate() @@ -421,6 +424,7 @@ export class Channel { if (this.userLimit !== other.userLimit) return false; if (this.voiceConnectionLimit !== other.voiceConnectionLimit) return false; if (this.rtcRegion !== other.rtcRegion) return false; + if (this.rtcP2p !== other.rtcP2p) return false; if (this.lastMessageId !== other.lastMessageId) return false; if (this.lastPinTimestamp?.getTime() !== other.lastPinTimestamp?.getTime()) return false; if (this.nsfw !== other.nsfw) return false; @@ -461,6 +465,7 @@ export class Channel { user_limit: this.userLimit, voice_connection_limit: this.voiceConnectionLimit, rtc_region: this.rtcRegion, + rtc_p2p: this.rtcP2p, last_message_id: this.lastMessageId, last_pin_timestamp: this.lastPinTimestamp?.toISOString() ?? undefined, permission_overwrites: Object.values(this.permissionOverwrites).map((o) => o.toJSON()), diff --git a/fluxer_app/src/features/forum/components/settings/ForumOverviewTab.tsx b/fluxer_app/src/features/forum/components/settings/ForumOverviewTab.tsx index 002bce01f..4a66b4026 100644 --- a/fluxer_app/src/features/forum/components/settings/ForumOverviewTab.tsx +++ b/fluxer_app/src/features/forum/components/settings/ForumOverviewTab.tsx @@ -77,6 +77,7 @@ function remoteValuesFor(forum: Channel): ForumFormInputs { content_warning_level: forum.contentWarningLevel ?? ContentWarningLevel.INHERIT, content_warning_text: forum.contentWarningText ?? '', rtc_region: null, + rtc_p2p: false, default_auto_archive_duration: forum.defaultAutoArchiveDuration ?? DEFAULT_THREAD_AUTO_ARCHIVE_DURATION, default_thread_rate_limit_per_user: forum.defaultThreadRateLimitPerUser, default_reaction: getDefaultReaction(forum), diff --git a/fluxer_app/src/features/gateway/events/EventRouter.ts b/fluxer_app/src/features/gateway/events/EventRouter.ts index 6ab4fb24b..23ed4bac0 100644 --- a/fluxer_app/src/features/gateway/events/EventRouter.ts +++ b/fluxer_app/src/features/gateway/events/EventRouter.ts @@ -76,6 +76,7 @@ import {handleCallDelete} from '@app/features/voice/events/CallDelete'; import {handleCallUpdate} from '@app/features/voice/events/CallUpdate'; import {handleEntranceSoundPlay} from '@app/features/voice/events/EntranceSoundPlay'; import {handleVoiceServerUpdate} from '@app/features/voice/events/VoiceServerUpdate'; +import {handleVoiceSignal} from '@app/features/voice/events/VoiceSignal'; import {handleVoiceStateUpdate} from '@app/features/voice/events/VoiceStateUpdate'; export interface GatewayGeoipPayload { @@ -164,6 +165,7 @@ export function createHandlerRegistry(): GatewayHandlerRegistry { registry.set('PRESENCE_UPDATE_BULK', handlePresenceUpdateBulk as GatewayEventHandler); registry.set('VOICE_STATE_UPDATE', handleVoiceStateUpdate as GatewayEventHandler); registry.set('VOICE_SERVER_UPDATE', handleVoiceServerUpdate as GatewayEventHandler); + registry.set('VOICE_SIGNAL', handleVoiceSignal as GatewayEventHandler); registry.set('CALL_CREATE', handleCallCreate as GatewayEventHandler); registry.set('CALL_UPDATE', handleCallUpdate as GatewayEventHandler); registry.set('CALL_DELETE', handleCallDelete as GatewayEventHandler); diff --git a/fluxer_app/src/features/gateway/transport/GatewayDispatchScheduling.ts b/fluxer_app/src/features/gateway/transport/GatewayDispatchScheduling.ts index b908d1ff5..e19efb9aa 100644 --- a/fluxer_app/src/features/gateway/transport/GatewayDispatchScheduling.ts +++ b/fluxer_app/src/features/gateway/transport/GatewayDispatchScheduling.ts @@ -12,6 +12,7 @@ const CRITICAL_DISPATCH_TYPES = new Set([ 'RESUMED', 'VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE', + 'VOICE_SIGNAL', 'CALL_CREATE', 'CALL_UPDATE', 'CALL_DELETE', diff --git a/fluxer_app/src/features/gateway/transport/GatewaySocket.ts b/fluxer_app/src/features/gateway/transport/GatewaySocket.ts index 67c9c4fc3..1c488d635 100644 --- a/fluxer_app/src/features/gateway/transport/GatewaySocket.ts +++ b/fluxer_app/src/features/gateway/transport/GatewaySocket.ts @@ -101,8 +101,26 @@ export interface GatewayVoiceStateUpdateParams { self_stream: boolean; viewer_stream_keys?: ReadonlyArray; connection_id: string | null; + p2p?: boolean; } +export interface GatewayVoiceSignalParams { + guild_id: string | null; + channel_id: string; + to: string; + data: object; +} + +export const VOICE_SIGNAL_MAX_JSON_BYTES = 16000; + +export const VoiceSignalSendResult = { + Sent: 'sent', + Oversized: 'oversized', + Unsent: 'unsent', +} as const; + +export type VoiceSignalSendResult = ValueOf; + export interface GatewaySocketOptions { token: string; apiVersion: number; @@ -157,7 +175,7 @@ function parseGatewayPayload(json: string): GatewayPayload { } function isGatewayErrorData(value: unknown): value is GatewayErrorData { - return isRecord(value) && typeof value.code === 'number' && typeof value.message === 'string'; + return isRecord(value) && typeof value.code === 'string' && typeof value.message === 'string'; } export interface GatewayDispatchReceipt { @@ -593,6 +611,17 @@ export class GatewaySocket extends EventEmitter { return this.sendPayload(this.buildVoiceStatePayload(params)); } + sendVoiceSignal(params: GatewayVoiceSignalParams): VoiceSignalSendResult { + if (!this.isConnected()) return VoiceSignalSendResult.Unsent; + const payload: GatewayPayload = {op: GatewayOpcodes.VOICE_SIGNAL, d: params}; + const bytes = new TextEncoder().encode(JSON.stringify(payload)).byteLength; + if (bytes > VOICE_SIGNAL_MAX_JSON_BYTES) { + this.log.warn(`Refusing voice signal of ${bytes} bytes`); + return VoiceSignalSendResult.Oversized; + } + return this.sendPayload(payload) ? VoiceSignalSendResult.Sent : VoiceSignalSendResult.Unsent; + } + requestGuildMembers(params: { guildId?: string; guildIds?: Array; diff --git a/fluxer_app/src/features/gateway/types/GatewayVoiceTypes.ts b/fluxer_app/src/features/gateway/types/GatewayVoiceTypes.ts index 3255a016b..bb5e1a9a9 100644 --- a/fluxer_app/src/features/gateway/types/GatewayVoiceTypes.ts +++ b/fluxer_app/src/features/gateway/types/GatewayVoiceTypes.ts @@ -19,6 +19,7 @@ export interface VoiceState { suppress?: boolean; member?: GuildMemberData; e2ee_capable?: boolean; + p2p?: boolean; } export interface CallVoiceState { diff --git a/fluxer_app/src/features/i18n/locales/ar/messages.po b/fluxer_app/src/features/i18n/locales/ar/messages.po index e2d332817..00337d024 100644 --- a/fluxer_app/src/features/i18n/locales/ar/messages.po +++ b/fluxer_app/src/features/i18n/locales/ar/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, zero {# رسالة محظورة} one {رسالة محظورة واحدة} two {رسالتان محظورتان} few {# رسائل محظورة} many {# رسالة محظورة} other {# رسالة محظورة}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# قناة تتابع} other {# قناة تتابع}} هذه القناة. تحويلها إلى قناة نصية سيزيل تلك المتابعات." @@ -2720,6 +2720,11 @@ msgstr "رمز تعبيري افتراضي. يمكنك استخدامه في أ msgid "A description is required." msgstr "الوصف مطلوب." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "لم يتم إنشاء اتصال مباشر. لا يمكنك سماع أو رؤية بعضكما البعض." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "تضمين غني أو معاينة رابط (وليس ملفًا مرف msgid "A separate profile for each community" msgstr "ملف شخصي منفصل لكل مجتمع" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "مكالمة عادية تمر عبر خادم وتعمل مع الجميع." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "أوقف المسؤولون الدعوات مؤقتًا، لذا لا ي #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "يمكن لأي شخص طلب حساب، ولكن يجب على المس msgid "Anyone in my communities" msgstr "أي شخص في مجتمعاتي" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "لا يمكن لأي شخص لا يستطيع الاتصال مباشرة المشاركة حتى يقوم شخص ما بتبديل المكالمة إلى مكالمة قياسية." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "يمكن لأي شخص يعثر عليك إرسال طلب. قم بإيقاف تشغيل هذا لاختيار المجموعات أدناه." @@ -5112,6 +5127,16 @@ msgstr "الصوت" msgid "Audio & video" msgstr "الصوت والفيديو" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "يتم إرسال الصوت والفيديو مباشرة بين المشاركين، بحد أقصى {maxParticipants, plural, one {# شخص} other {# أشخاص}}. يوافق الجميع قبل الانضمام ويمكنهم رؤية عنوان IP الخاص بكل منهم." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "يتم إرسال الصوت والفيديو مباشرة بين المشاركين." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "اقتراح الإكمال التلقائي: {label}، {selectedOptionP #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "تلقائي" @@ -6936,15 +6961,15 @@ msgstr "لا يمكن حذف الحساب" msgid "Capture" msgstr "التقاط" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "التقاط صوت التطبيق" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "التقاط صوت سطح المكتب" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "التقاط صوت الجهاز" @@ -7017,7 +7042,7 @@ msgstr "تم نسخ معرف الفئة" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "اسم الفئة" @@ -7554,7 +7579,7 @@ msgstr "أقسام تفاصيل القناة" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "تم نسخ معرف القناة" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "رابط القناة" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "تم نسخ رابط القناة" @@ -7604,7 +7629,7 @@ msgstr "إدارة القنوات" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "القناة متزامنة مع الفئة الرئيسية" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "موضوع القناة طويل جدًا." @@ -7668,8 +7693,8 @@ msgstr "اختيار نوع القناة" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "تم تحديث القناة" @@ -9421,7 +9446,7 @@ msgstr "التأكيد قبل الانضمام إلى القنوات الصوت #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "التأكيد قبل بدء المكالمات" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "المحادثات" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "تحويل" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "تم تحويل القناة إلى قناة إعلانات" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "تحويل هذه القناة إلى قناة نصية سيؤدي إلى إزالة كل قناة تتبعها." @@ -9949,7 +9974,7 @@ msgstr "نسخ معرف القناة" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "نسخ رابط القناة" @@ -10379,13 +10404,18 @@ msgstr "تعذّر إكمال تسجيل الدخول. حاول مرة أخرى. msgid "Couldn't connect to {domain}. Try again." msgstr "تعذر الاتصال بـ {domain}. حاول مرة أخرى." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "تعذر الاتصال بـ {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "تعذّر الاتصال الصوتي. يُرجى المحاولة مرة أخرى." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "تعذر تحويل هذه القناة" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "تعذّر تنزيل تحديث تطبيق سطح المكتب" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "تعذر تكرار هذه القناة" @@ -10511,6 +10541,11 @@ msgstr "تعذّر تجاهل طلب الصداقة. يُرجى المحاولة msgid "Couldn't join community" msgstr "تعذّر الانضمام إلى المجتمع" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "تعذر الانضمام إلى المكالمة من نظير إلى نظير. حاول مرة أخرى." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "تعذر فتح بوابة الفوترة" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "تعذر تثبيت هذه المحادثة" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "تعذر تنفيذ هذا الأمر: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "تعذر حفظ إعدادات القناة" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "تعذر إلغاء متابعة هذه القناة" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "تعذر إلغاء تثبيت هذه المحادثة" @@ -11374,7 +11409,7 @@ msgstr "مصدر مخصص" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "مخصص" @@ -11938,7 +11973,7 @@ msgstr "افتراضي" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "حذف \"{stickerName}\"؟ لا يمكن التراجع عن هذا ال #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "حذف {channelLabel}؟ لا يمكن التراجع عن هذا الإجراء." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "صوت الجهاز ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "صوت الجهاز فقط" @@ -12694,7 +12729,7 @@ msgstr "تفاصيل الجهاز" msgid "Device revoked" msgstr "تم إلغاء ربط الجهاز" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "اختيار الجهاز" @@ -12758,6 +12793,11 @@ msgstr "أحجام وأعداد أعضاء مختلفة" msgid "Dim strikethrough text" msgstr "تعتيم النص المشطوب" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "اتصال مباشر. الوسائط مشفرة بين المشاركين." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "الشريط الجانبي للرسائل المباشرة" msgid "DNS TXT record" msgstr "سجل DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "لا تسأل مرة أخرى، وافق دائمًا" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "تكرار" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "تكرار القناة" @@ -13825,7 +13870,7 @@ msgstr "تعديل الفئة" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "تعديل القناة" @@ -15004,7 +15049,7 @@ msgstr "مفتاح الهروب" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "الخروج من وضع لوحة المفاتيح بمفتاح الهروب" @@ -15047,6 +15092,11 @@ msgstr "سيحصل كل مجتمع يتابع هذه القناة على نسخ msgid "Everyone" msgstr "الجميع" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "يحصل الجميع في المكالمة على جودة بث Plutonium مجانًا: دقة مصدر تصل إلى 4K و 60 إطارًا في الثانية." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "تم منح {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "يمنح جميع الأذونات ويتجاوز قيود القنوات. إذن حساس للغاية." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "تحديد دقيق" @@ -17417,7 +17467,7 @@ msgstr "اليونانية" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "عرض شبكي" @@ -17909,7 +17959,7 @@ msgstr "إخفاء المشاركين" msgid "Hide password" msgstr "إخفاء كلمة المرور" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "إخفاء صورة المعاينة المصغرة" @@ -18322,11 +18372,11 @@ msgstr "تجاهل" msgid "Ignore friend request" msgstr "تجاهل طلب الصداقة" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "تجاهل الأجهزة المادية" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "تجاهل وسائط الإدخال" @@ -18357,7 +18407,7 @@ msgstr "هل تريد تجاهل طلب الصداقة من {displayName}؟" msgid "Ignore this channel's slowmode cooldown." msgstr "تجاهل فترة انتظار الوضع البطيء في هذه القناة." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "تجاهل الأجهزة الافتراضية" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "جهاز الإدخال" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "مستوى الإدخال" @@ -18865,7 +18915,7 @@ msgstr "مراقبة الإدخال" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "ملف تعريف الإدخال" @@ -19431,6 +19481,12 @@ msgstr "التذبذب:" msgid "Join" msgstr "انضمام" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "انضم" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "الانضمام إلى مجتمع" msgid "Join a community to unlock stickers." msgstr "انضم إلى مجتمع لتتمكن من استخدام الملصقات." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "الانضمام إلى مكالمة نظير إلى نظير؟" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "طريقة الانضمام" msgid "Join on this device" msgstr "الانضمام من هذا الجهاز" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "الانضمام إلى مكالمات الند للند دون طلب" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "مغادرة {threadName}؟ إنها خاصة، لذا لن تتمكن msgid "Leave blank for a random username" msgstr "اتركه فارغًا لاختيار اسم مستخدم عشوائي" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "مغادرة المكالمة" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "يسمح لـ CSS المخصص بإظهار مناطق شفافة في النافذة بعد إعادة تشغيل تطبيق سطح المكتب." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "يتيح لمجتمعات أخرى متابعة هذه القناة والحصول على نسخ مما تنشره." @@ -20729,6 +20797,11 @@ msgstr "لويزيانا" msgid "Low" msgstr "منخفض" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "زمن استجابة أقل." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "ميشيغان" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "مينيسوتا" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "عكس الكاميرا" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "ملصقي الرائع" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "فئتي" @@ -24444,7 +24517,7 @@ msgstr "تابع فقط إذا كنت قد بدأت تسجيل الدخول هذ msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "يتم قبول بطاقات الائتمان فقط. لا يمكن استخدام بطاقات الخصم والبطاقات مسبقة الدفع في التحقق من المحتوى للبالغين." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "مكبرات الصوت الافتراضية فقط" @@ -24522,7 +24595,7 @@ msgstr "سيتمكن الأعضاء والأدوار المحددة فقط من msgid "Only show message previews for DMs with unread messages" msgstr "إظهار معاينات الرسائل فقط للرسائل المباشرة التي تحتوي على رسائل غير مقروءة" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "مكبرات الصوت فقط" @@ -24759,7 +24832,7 @@ msgstr "فتح تفاصيل القناة لـ {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "فتح الدردشة" @@ -24840,7 +24913,7 @@ msgstr "فتح الرمز التعبيري في المتصفح" msgid "Open emoji picker" msgstr "فتح منتقي الرموز التعبيرية" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "فتح نافذة استقبال الأحداث" @@ -25280,6 +25353,16 @@ msgstr "مواضيع نشطة أخرى" msgid "Other devices" msgstr "أجهزة أخرى" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "يمكن للمشاركين الآخرين رؤية عنوان IP الخاص بك." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "يمكن للمشاركين الآخرين في مكالمة نظير إلى نظير رؤية عنوان IP الخاص بك." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "تعذر نقل الملكية إلى <0>{username} في هذا ال msgid "Ownership transferred" msgstr "تم نقل الملكية" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "المكالمات من نظير إلى نظير غير متاحة حاليًا." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "مكالمات نظير إلى نظير فقط" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "تم تثبيت المجموعة" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "تم تثبيت المحادثة الجماعية" @@ -26536,7 +26635,7 @@ msgstr "زر المعاينة" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "معاينة الكاميرا" @@ -27107,7 +27206,7 @@ msgstr "مفتاح الضغط للتحدث" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "الضغط للتحدث" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "قصّر الرسالة وحاول مرة أخرى." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "قصّر الموضوع وحاول مرة أخرى." @@ -31488,7 +31587,7 @@ msgstr "إظهار عناصر تحكم مستوى الصوت للاتصال" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "إظهار اختصارات قائمة السياق" @@ -31573,7 +31672,7 @@ msgstr "إظهار عدد أقل من المؤثرات الصوتية" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "إظهار حلقة التركيز على مربع نص الدردشة" @@ -31701,7 +31800,7 @@ msgstr "إظهار القنوات المكتومة" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "إظهار الكاميرا الخاصة بي" @@ -31742,7 +31841,7 @@ msgstr "عرض المشاركات الجديدة كغير مقروءة" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "إظهار المشاركين بدون فيديو" @@ -32144,7 +32243,7 @@ msgstr "مكالمة صامتة" msgid "Silent calls" msgstr "مكالمات صامتة" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "صامت عندما لا يحتوي جهاز الالتقاط على مدخل صوتي خاص به" @@ -32655,8 +32754,8 @@ msgstr "حدث خطأ ما." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "حدث خطأ ما. يرجى المحاولة مرة أخرى بعد قليل." @@ -32750,7 +32849,7 @@ msgstr "الأصوات" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "المصدر" @@ -32859,7 +32958,7 @@ msgstr "تحدث الآن. اضغط على إيقاف عند الانتهاء، #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "مكبر الصوت" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "بدء محادثة جماعية" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "بدء مكالمة فيديو؟" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "بدء مكالمة صوتية؟" @@ -33092,6 +33191,16 @@ msgstr "يجب أن يكون تاريخ البدء قبل تاريخ الانت msgid "Start drag and drop" msgstr "بدء السحب والإفلات" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "البدء في وضع الند للند" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "البدء في وضع الند للند؟" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "بدء مصغر" msgid "Start over" msgstr "البدء من جديد" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "بدء مكالمة نظير إلى نظير" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "ابدأ عمليات البحث بـ <0>{peoplePrefix} <1>{textChan msgid "Start speaking" msgstr "بدء النطق" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "بدء مكالمة قياسية" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "لوحة الحالة" msgid "Status:" msgstr "الحالة:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "البقاء" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "إيقاف" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "إيقاف القناة كقناة إعلانات؟" @@ -33921,6 +34045,11 @@ msgstr "التبديل إلى المجتمع السابق أو الرسائل ا msgid "Switch to single column" msgstr "التبديل إلى عمود واحد" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "التبديل إلى مكالمة قياسية" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "تم إلغاء تثبيت المجموعة" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "تم إلغاء تثبيت المحادثة الجماعية" @@ -37727,6 +37856,11 @@ msgstr "بريد إلكتروني غير مُتحقَّق منه" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "تساعد الوسوم الناس في العثور على مجتمعك، ويمكنك إضافة ما يصل إلى {DISCOVERY_MAX_TAGS}. تظهر الوسوم في بحث الاكتشاف." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "حتى {maxParticipants, plural, one {# شخص} other {# أشخاص}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "جارٍ تحميل الرموز التعبيرية" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "إرسال الفيديو:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "إعدادات الفيديو" @@ -39570,7 +39704,7 @@ msgstr "التسجيل الصوتي غير مدعوم في هذا المتصفح #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "منطقة الصوت" @@ -39582,7 +39716,7 @@ msgstr "شريط تذكير استعادة الجلسة الصوتية" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "إعدادات الصوت" diff --git a/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json b/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json index 9ee371a9a..1d8b3428c 100644 --- a/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json +++ b/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json @@ -38,6 +38,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -121,6 +124,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -285,6 +291,9 @@ { "msgid": "Online" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -783,6 +792,9 @@ { "msgid": "Output" }, + { + "msgid": "P2P" + }, { "msgid": "Permanent" }, @@ -1448,6 +1460,9 @@ { "msgid": "Online" }, + { + "msgid": "P2P" + }, { "msgid": "Passkey" }, @@ -1921,6 +1936,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -2062,6 +2080,9 @@ { "msgid": "A default emoji. You can use it anywhere on {productName}." }, + { + "msgid": "A direct connection couldn't be made. You can't hear or see each other." + }, { "msgid": "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." }, @@ -2071,6 +2092,9 @@ { "msgid": "A separate profile for each community" }, + { + "msgid": "A standard call goes through a server and works for everyone." + }, { "msgid": "AV1" }, @@ -2200,6 +2224,9 @@ { "msgid": "Anyone in my communities" }, + { + "msgid": "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." + }, { "msgid": "Anyone who finds you can send a request. Turn this off to pick groups below." }, @@ -2245,6 +2272,12 @@ { "msgid": "Attachment {pendingFilename}" }, + { + "msgid": "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." + }, + { + "msgid": "Audio and video go directly between participants." + }, { "msgid": "Audio muted" }, @@ -2605,6 +2638,9 @@ { "msgid": "Couldn't connect to {domain}. Try again." }, + { + "msgid": "Couldn't connect to {names}" + }, { "msgid": "Couldn't convert this channel" }, @@ -2620,6 +2656,9 @@ { "msgid": "Couldn't duplicate this channel" }, + { + "msgid": "Couldn't join the P2P call. Try again." + }, { "msgid": "Couldn't load posts. Try again later." }, @@ -2791,6 +2830,9 @@ { "msgid": "Device volume" }, + { + "msgid": "Direct connection. Media is encrypted between participants." + }, { "msgid": "Disable direct messages and friend requests" }, @@ -2965,6 +3007,9 @@ { "msgid": "Every community that follows this channel gets a copy. If you edit or delete it later, the copies change too." }, + { + "msgid": "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." + }, { "msgid": "Expires in {remainingSeconds}s" }, @@ -3262,6 +3307,12 @@ { "msgid": "Italic" }, + { + "msgid": "Join P2P calls without asking" + }, + { + "msgid": "Join a P2P call?" + }, { "msgid": "Join this thread to follow it in your channel list and get notifications." }, @@ -3400,6 +3451,9 @@ { "msgid": "Logo" }, + { + "msgid": "Lower latency." + }, { "msgid": "Make sure this code matches the one in the {PRODUCT_NAME} app." }, @@ -3697,6 +3751,21 @@ { "msgid": "Other active threads" }, + { + "msgid": "Other participants can see your IP address." + }, + { + "msgid": "Other participants in a P2P call can see your IP address." + }, + { + "msgid": "P2P" + }, + { + "msgid": "P2P calls are unavailable right now." + }, + { + "msgid": "P2P calls only" + }, { "msgid": "Passkey sign-in is unavailable here." }, @@ -4426,15 +4495,30 @@ { "msgid": "Stack traces" }, + { + "msgid": "Start P2P call" + }, + { + "msgid": "Start in P2P mode" + }, + { + "msgid": "Start in P2P mode?" + }, { "msgid": "Start in the tray instead of opening the window when launched at login" }, + { + "msgid": "Start standard call" + }, { "msgid": "Started by {name}" }, { "msgid": "Static Neko" }, + { + "msgid": "Stay" + }, { "msgid": "Stay focused on a conversation with a thread, a temporary text channel." }, @@ -4495,6 +4579,9 @@ { "msgid": "Switch to account {slot}" }, + { + "msgid": "Switch to standard call" + }, { "msgid": "Switch to the new price?" }, @@ -4804,6 +4891,9 @@ { "msgid": "Up to 720p at 30 FPS" }, + { + "msgid": "Up to {maxParticipants, plural, one {# person} other {# people}}." + }, { "msgid": "Update available" }, @@ -5481,6 +5571,10 @@ { "msgctxt": "voice-connection-status", "msgid": "Dismiss" + }, + { + "msgctxt": "voice-p2p-consent", + "msgid": "Join" } ], "es-419": [ @@ -5595,6 +5689,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PiP" }, @@ -5987,6 +6084,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PiP" }, @@ -6324,6 +6424,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -6814,6 +6917,9 @@ { "msgid": "Options" }, + { + "msgid": "P2P" + }, { "msgid": "Participants" }, @@ -7229,6 +7335,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -7345,6 +7454,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -7467,6 +7579,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -7834,6 +7949,9 @@ { "msgid": "Online" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -8329,6 +8447,9 @@ { "msgid": "Output" }, + { + "msgid": "P2P" + }, { "msgid": "Passkey" }, @@ -8895,6 +9016,9 @@ { "msgid": "Output" }, + { + "msgid": "P2P" + }, { "msgid": "Passkey" }, @@ -9293,6 +9417,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -9414,6 +9541,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -9541,6 +9671,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -9927,6 +10060,9 @@ { "msgid": "Open {productName}" }, + { + "msgid": "P2P" + }, { "msgid": "Passkey" }, @@ -10507,6 +10643,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "Pause" }, @@ -10951,6 +11090,9 @@ { "msgid": "Online" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -11327,6 +11469,9 @@ { "msgid": "Online" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -11827,6 +11972,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "Permanent" }, @@ -12244,6 +12392,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -12419,6 +12570,9 @@ { "msgid": "Online" }, + { + "msgid": "P2P" + }, { "msgid": "Permanent" }, @@ -12845,6 +12999,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -13008,6 +13165,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -13344,6 +13504,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -13513,6 +13676,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, @@ -13847,6 +14013,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "Searching..." }, @@ -13964,6 +14133,9 @@ { "msgid": "OP" }, + { + "msgid": "P2P" + }, { "msgid": "PgDn" }, diff --git a/fluxer_app/src/features/i18n/locales/bg/messages.po b/fluxer_app/src/features/i18n/locales/bg/messages.po index d4760d008..87bf9b670 100644 --- a/fluxer_app/src/features/i18n/locales/bg/messages.po +++ b/fluxer_app/src/features/i18n/locales/bg/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# блокирано съобщение} other {# блокирани съобщения}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# канал следва} other {# канала следват}} този канал. Преобразуването му в текстов канал ще премахне тези последователи." @@ -2720,6 +2720,11 @@ msgstr "Стандартна емотикона. Можете да я изпол msgid "A description is required." msgstr "Изисква се описание." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Директна връзка не може да бъде установена. Не можете да се чувате или виждате." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "вграден елемент или визуализация на ли msgid "A separate profile for each community" msgstr "Отделен профил за всяка общност" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Стандартното обаждане минава през сървър и работи за всички." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Администраторите спряха поканите и в м #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Всеки може да поиска акаунт, но админис msgid "Anyone in my communities" msgstr "Всеки от моите общности" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Всеки, който не може да се свърже директно, не може да участва, докато някой не превключи разговора към стандартен." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Всеки, който ви намери, може да изпрати заявка. Изключете това, за да изберете групи по-долу." @@ -5112,6 +5127,16 @@ msgstr "Аудио" msgid "Audio & video" msgstr "Аудио и видео" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Аудиото и видеото преминават директно между участниците, до {maxParticipants, plural, one {# човек} other {# души}}. Всички се съгласяват преди да се присъединят и могат да виждат IP адреса на всеки." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Аудиото и видеото преминават директно между участниците." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Предложение за автоматично довършване: #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Автоматично" @@ -6936,15 +6961,15 @@ msgstr "Акаунтът не може да бъде изтрит" msgid "Capture" msgstr "Запис" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Запис на звука от приложението" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Запис на звука от работния плот" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Запис на звука от устройството" @@ -7017,7 +7042,7 @@ msgstr "ID на категорията е копирано" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Име на категорията" @@ -7554,7 +7579,7 @@ msgstr "Раздели с подробности за канала" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID на канала е копирано" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "линк към канал" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Линкът към канала е копиран" @@ -7604,7 +7629,7 @@ msgstr "Управление на канали" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Каналът е синхронизиран с родителската категория" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Темата на канала е твърде дълга." @@ -7668,8 +7693,8 @@ msgstr "Избор на тип канал" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Каналът е актуализиран" @@ -9421,7 +9446,7 @@ msgstr "Потвърждаване преди присъединяване къ #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Потвърждаване преди започване на разговори" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Разговори" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Конвертиране" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Преобразуван е в канал за съобщения" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Преобразуването на този канал в текстов канал ще премахне всички последващи канали." @@ -9949,7 +9974,7 @@ msgstr "Копирай ID на канала" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Копирай линка към канала" @@ -10379,13 +10404,18 @@ msgstr "Влизането не можа да бъде завършено. Оп msgid "Couldn't connect to {domain}. Try again." msgstr "Не успяхме да се свържем с {domain}. Опитайте отново." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Не успяхме да се свържем с {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Не успяхме да се свържем с гласовия канал. Опитай отново." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Не успяхме да преобразуваме този канал" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Неуспешно изтегляне на актуализацията за настолното приложение" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Не успяхме да дублираме този канал" @@ -10511,6 +10541,11 @@ msgstr "Поканата за приятелство не можа да бъде msgid "Couldn't join community" msgstr "Неуспешно присъединяване към общността" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Не успяхме да се присъединим към P2P обаждането. Опитайте отново." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Неуспешно отваряне на портала за плащане" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Разговорът не можа да бъде закачен" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Командата не можа да бъде изпълнена: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Не успях да запазя настройките на канала" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Не успяхме да прекратим следването на този канал" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Разговорът не можа да бъде откачен" @@ -11374,7 +11409,7 @@ msgstr "Персонализиран източник" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "По избор" @@ -11938,7 +11973,7 @@ msgstr "По подразбиране" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Изтриване на „{stickerName}“? Това действие #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Изтриване на {channelLabel}? Това действие не може да бъде отменено." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Аудио на устройството ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Само аудио от устройството" @@ -12694,7 +12729,7 @@ msgstr "Подробности за устройството" msgid "Device revoked" msgstr "Достъпът на устройството е отнет" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Избор на устройство" @@ -12758,6 +12793,11 @@ msgstr "Различни размери и брой членове" msgid "Dim strikethrough text" msgstr "Приглушаване на зачеркнатия текст" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Директна връзка. Медията е криптирана между участниците." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Странична лента с DM" msgid "DNS TXT record" msgstr "DNS TXT запис" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Не питай отново, винаги се съгласявай" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Дублиране" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Дублиране на канала" @@ -13825,7 +13870,7 @@ msgstr "Редактиране на категория" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Редактиране на канала" @@ -15004,7 +15049,7 @@ msgstr "Клавиш Esc" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Клавишът Esc изключва режим „Клавиатура“" @@ -15047,6 +15092,11 @@ msgstr "Всяка общност, която следва този канал, msgid "Everyone" msgstr "Всеки" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Всички в разговора получават безплатно Plutonium стрийм качество: до 4K резолюция и 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Предоставени {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Предоставя всички разрешения и заобикаля ограниченията на канала. Изключително чувствително разрешение." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Подробен избор" @@ -17417,7 +17467,7 @@ msgstr "Гръцки" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Мрежов изглед" @@ -17909,7 +17959,7 @@ msgstr "Скрий участниците" msgid "Hide password" msgstr "Скрий паролата" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Скрий миниатюрата за предварителен преглед" @@ -18322,11 +18372,11 @@ msgstr "Игнориране" msgid "Ignore friend request" msgstr "Игнориране на покана за приятелство" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Игнориране на хардуерни устройства" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Игнориране на входни медии" @@ -18357,7 +18407,7 @@ msgstr "Да игнорираш ли поканата за приятелств msgid "Ignore this channel's slowmode cooldown." msgstr "Игнориране на бавния режим в този канал." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Игнориране на виртуални" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Входно устройство" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Входно ниво" @@ -18865,7 +18915,7 @@ msgstr "Наблюдение на въвеждането" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Входен профил" @@ -19431,6 +19481,12 @@ msgstr "Джитър:" msgid "Join" msgstr "Присъединяване" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Присъединяване" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Присъединяване към общност" msgid "Join a community to unlock stickers." msgstr "Присъедини се към общност, за да отключиш стикери." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Присъединяване към P2P разговор?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Начин на присъединяване" msgid "Join on this device" msgstr "Присъединяване от това устройство" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Присъединяване към P2P разговори без потвърждение" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Напускане на {threadName}? Той е частен, така msgid "Leave blank for a random username" msgstr "Остави празно за случайно потребителско име" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Напусни разговора" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Позволява на персонализирания CSS да показва прозрачни зони на прозореца след рестартиране на настолното приложение." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Позволява на други общности да следят този канал и да получават копия на това, което публикувате." @@ -20729,6 +20797,11 @@ msgstr "Луизиана" msgid "Low" msgstr "Ниско" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "По-ниска латентност." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Мичиган" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Минесота" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Огледално изображение на камерата" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Моят страхотен стикер" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Моята категория" @@ -24444,7 +24517,7 @@ msgstr "Продължете само ако сте започнали това msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Приемат се само кредитни карти. Дебитните и предплатените карти не могат да се използват за проверката за съдържание за възрастни." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Само високоговорители по подразбиране" @@ -24522,7 +24595,7 @@ msgstr "Само избрани членове и роли ще могат да msgid "Only show message previews for DMs with unread messages" msgstr "Показване на визуализации само за DM с непрочетени съобщения" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Само високоговорители" @@ -24759,7 +24832,7 @@ msgstr "Отваряне на подробности за канала „{chann #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Отваряне на чата" @@ -24840,7 +24913,7 @@ msgstr "Отваряне на емоджи в браузър" msgid "Open emoji picker" msgstr "Отваряне на палитрата с емоджита" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Отваряне на приемника на събития" @@ -25280,6 +25353,16 @@ msgstr "Други активни теми" msgid "Other devices" msgstr "Други устройства" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Други участници могат да видят вашия IP адрес." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Други участници в P2P разговор могат да видят вашия IP адрес." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Собствеността не можа да бъде прехвърл msgid "Ownership transferred" msgstr "Собствеността е прехвърлена" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "P2P разговорите в момента са недостъпни." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Само P2P разговори" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Групата е закачена" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Груповото DM е закачено" @@ -26536,7 +26635,7 @@ msgstr "Бутон за преглед" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Преглед на камерата" @@ -27107,7 +27206,7 @@ msgstr "Клавиш за „Натисни и говори“" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Натисни и говори" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Съкрати съобщението и опитай отново." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Съкрати темата и опитай отново." @@ -31488,7 +31587,7 @@ msgstr "Показване на контролите за силата на зв #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Показване на клавишните комбинации в контекстните менюта" @@ -31573,7 +31672,7 @@ msgstr "Покажи по-малко звукови ефекти" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Показване на рамка за фокус върху текстовото поле за чат" @@ -31701,7 +31800,7 @@ msgstr "Показване на заглушени канали" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Показване на моята камера" @@ -31742,7 +31841,7 @@ msgstr "Показвай нови публикации като непрочет #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Показване на участници без видео" @@ -32144,7 +32243,7 @@ msgstr "Безшумно повикване" msgid "Silent calls" msgstr "Безшумни повиквания" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Тихо, когато устройството за заснемане няма собствен аудио вход" @@ -32655,8 +32754,8 @@ msgstr "Нещо се обърка." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Нещо се обърка. Опитай отново след малко." @@ -32750,7 +32849,7 @@ msgstr "Звуци" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Източник" @@ -32859,7 +32958,7 @@ msgstr "Говори сега. Натисни „Спиране“, когато #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Високоговорител" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Започване на групово DM" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Да започнеш ли видеоразговор?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Да започнеш ли гласово повикване?" @@ -33092,6 +33191,16 @@ msgstr "Началната дата трябва да е преди крайна msgid "Start drag and drop" msgstr "Започване на плъзгане и пускане" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Стартирай в P2P режим" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Стартиране в P2P режим?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Стартирай свит" msgid "Start over" msgstr "Започване отначало" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Започни P2P разговор" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Започни търсенето с <0>{peoplePrefix} <1>{textChann msgid "Start speaking" msgstr "Започване на прочитането" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Започни стандартно обаждане" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Панел за състояние" msgid "Status:" msgstr "Състояние:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Остани" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Спиране" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Да спре да бъде канал за съобщения?" @@ -33921,6 +34045,11 @@ msgstr "Превключване към предишна общност или D msgid "Switch to single column" msgstr "Превключване към една колона" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Превключи към стандартно обаждане" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Групата е откачена" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Груповото DM е откачено" @@ -37727,6 +37856,11 @@ msgstr "Непотвърден имейл" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "До {DISCOVERY_MAX_TAGS} етикета помагат на хората да открият общността ти. Те се показват при търсене в Откриване." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "До {maxParticipants, plural, one {# човек} other {# човека}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Качване на емоджита" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Изпращане на видео:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Настройки на видеото" @@ -39570,7 +39704,7 @@ msgstr "Гласовият запис не се поддържа в този б #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Гласов регион" @@ -39582,7 +39716,7 @@ msgstr "Лента за възстановяване на гласова сес #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Настройки на гласа" diff --git a/fluxer_app/src/features/i18n/locales/cs/messages.po b/fluxer_app/src/features/i18n/locales/cs/messages.po index 0f8beea74..4d3ca8f08 100644 --- a/fluxer_app/src/features/i18n/locales/cs/messages.po +++ b/fluxer_app/src/features/i18n/locales/cs/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# zablokovaná zpráva} few {# zablokované zprávy} many {# zablokované zprávy} other {# zablokovaných zpráv}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# kanál ho sleduje} other {# kanálů ho sleduje}} tento kanál. Převedením na textový kanál se tito sledující odeberou." @@ -2720,6 +2720,11 @@ msgstr "Výchozí emoji. Můžete ji použít kdekoli na {productName}." msgid "A description is required." msgstr "Je vyžadován popis." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Přímé spojení se nepodařilo navázat. Neslyšíte se ani nevidíte." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "formátovaný vložený obsah nebo náhled odkazu (nikoli nahraný soubo msgid "A separate profile for each community" msgstr "Samostatný profil pro každé společenství" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Běžný hovor prochází přes server a funguje pro všechny." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Správci pozastavili pozvánky – momentálně se nemůžete připojit. #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "O účet může požádat kdokoli, ale správce ho musí schválit." msgid "Anyone in my communities" msgstr "Kdokoliv z mých komunit" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Pokud se někdo nemůže připojit přímo, nemůže se zúčastnit, dokud někdo nepřepne hovor na standardní." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Kdokoli vás najde, vám může poslat žádost. Vypněte tuto možnost a vyberte skupiny níže." @@ -5112,6 +5127,16 @@ msgstr "Zvuk" msgid "Audio & video" msgstr "Zvuk a video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Zvuk a video jdou přímo mezi účastníky, až pro {maxParticipants, plural, one {# osobu} few {# osoby} other {# osob}}. Všichni se před připojením shodnou a uvidí IP adresy ostatních." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Zvuk a obraz jdou přímo mezi účastníky." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Návrh automatického doplňování: {label}, {selectedOptionPosition} z #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automaticky" @@ -6936,15 +6961,15 @@ msgstr "Účet nelze smazat" msgid "Capture" msgstr "Zachytávání" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Zachytávat zvuk aplikace" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Zachytávat zvuk z počítače" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Zachytávat zvuk zařízení" @@ -7017,7 +7042,7 @@ msgstr "ID kategorie zkopírováno" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Název kategorie" @@ -7554,7 +7579,7 @@ msgstr "Sekce podrobností kanálu" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID kanálu zkopírováno" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "odkaz na kanál" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Odkaz na kanál zkopírován" @@ -7604,7 +7629,7 @@ msgstr "Správa kanálů" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Kanál synchronizován s nadřazenou kategorií" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Téma kanálu je příliš dlouhé." @@ -7668,8 +7693,8 @@ msgstr "Výběr typu kanálu" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Kanál aktualizován" @@ -9421,7 +9446,7 @@ msgstr "Vyžadovat potvrzení před připojením k hlasovým kanálům" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Vyžadovat potvrzení před zahájením hovorů" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Konverzace" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Převést" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Kanál byl převeden na kanál oznámení" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Převodem tohoto kanálu na textový kanál se odstraní všechny kanály, které ho sledují." @@ -9949,7 +9974,7 @@ msgstr "Kopírovat ID kanálu" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Kopírovat odkaz na kanál" @@ -10379,13 +10404,18 @@ msgstr "Přihlášení se nepodařilo dokončit. Zkuste to znovu." msgid "Couldn't connect to {domain}. Try again." msgstr "Nepodařilo se připojit k {domain}. Zkuste to znovu." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Nepodařilo se připojit k {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Nepodařilo se připojit k hlasovému hovoru. Zkuste to prosím znovu." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Nepodařilo se převést tento kanál" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Nepodařilo se stáhnout aktualizaci desktopové aplikace" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Nepodařilo se zkopírovat tento kanál" @@ -10511,6 +10541,11 @@ msgstr "Nepodařilo se ignorovat žádost o přátelství. Zkuste to znovu." msgid "Couldn't join community" msgstr "Nepodařilo se připojit ke komunitě" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Nepodařilo se připojit k P2P hovoru. Zkuste to znovu." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Portál pro platby se nepodařilo otevřít" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Konverzaci se nepodařilo připnout" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Nepodařilo se spustit příkaz: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Nastavení kanálu se nepodařilo uložit" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Nepodařilo se zrušit sledování tohoto kanálu" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Konverzaci se nepodařilo odepnout" @@ -11374,7 +11409,7 @@ msgstr "Vlastní zdroj" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Vlastní" @@ -11938,7 +11973,7 @@ msgstr "Výchozí" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Smazat „{stickerName}“? Tuto akci nelze vrátit zpět." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Smazat {channelLabel}? Nelze vrátit zpět." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Zvuk zařízení ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Zvuk zařízení" @@ -12694,7 +12729,7 @@ msgstr "Podrobnosti o zařízení" msgid "Device revoked" msgstr "Zařízení odhlášeno" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Výběr zařízení" @@ -12758,6 +12793,11 @@ msgstr "Různé velikosti a počty členů" msgid "Dim strikethrough text" msgstr "Snížit výraznost přeškrtnutého textu" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Přímé připojení. Média jsou šifrována mezi účastníky." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Postranní panel přímých zpráv" msgid "DNS TXT record" msgstr "Záznam DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Už se neptejte, vždy souhlasit" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplikovat" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplikovat kanál" @@ -13825,7 +13870,7 @@ msgstr "Upravit kategorii" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Upravit kanál" @@ -15004,7 +15049,7 @@ msgstr "Klávesa Escape" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Klávesa Escape ukončí režim navigace klávesnicí" @@ -15047,6 +15092,11 @@ msgstr "Každá komunita, která tento kanál sleduje, dostane kopii. Pokud ji p msgid "Everyone" msgstr "Všichni" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Všichni v hovoru získají kvalitu streamu Plutonium zdarma: až 4K zdrojové rozlišení a 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Udělena oprávnění {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Uděluje všechna oprávnění a obchází omezení kanálů. Jde o vysoce citlivé oprávnění." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Podrobný výběr" @@ -17417,7 +17467,7 @@ msgstr "Řečtina" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Zobrazení mřížky" @@ -17909,7 +17959,7 @@ msgstr "Skrýt účastníky" msgid "Hide password" msgstr "Skrýt heslo" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Skrýt miniaturu náhledu" @@ -18322,11 +18372,11 @@ msgstr "Ignorovat" msgid "Ignore friend request" msgstr "Ignorovat žádost o přátelství" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignorovat hardwarová zařízení" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignorovat vstupní média" @@ -18357,7 +18407,7 @@ msgstr "Ignorovat žádost o přátelství od uživatele {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignorovat čekací dobu pomalého režimu v tomto kanálu." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignorovat virtuální" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Vstupní zařízení" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Vstupní úroveň" @@ -18865,7 +18915,7 @@ msgstr "Monitorování vstupu" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Vstupní profil" @@ -19431,6 +19481,12 @@ msgstr "Kolísání:" msgid "Join" msgstr "Připojit se" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Připojit" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Připojit se ke komunitě" msgid "Join a community to unlock stickers." msgstr "Připojte se ke komunitě a zpřístupněte si samolepky." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Připojit se k P2P hovoru?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Způsob připojení" msgid "Join on this device" msgstr "Připojit se na tomto zařízení" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Připojit se k P2P hovorům bez potvrzení" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Opustit {threadName}? Je to soukromé, takže se sem budete moci vrátit msgid "Leave blank for a random username" msgstr "Ponechte prázdné pro náhodné uživatelské jméno" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Opustit hovor" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Po restartování desktopové aplikace umožní pomocí vlastního CSS zobrazit průhledné oblasti okna." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Umožní ostatním komunitám sledovat tento kanál a dostávat kopie toho, co publikujete." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Nízká" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Nižší latence." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Zrcadlit kameru" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Moje super samolepka" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Moje kategorie" @@ -24444,7 +24517,7 @@ msgstr "Pokračujte, pouze pokud jste toto přihlášení zahájili na zařízen msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Přijímány jsou pouze kreditní karty. Debetní a předplacené karty nelze použít k ověření věku pro obsah pro dospělé." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Pouze výchozí reproduktory" @@ -24522,7 +24595,7 @@ msgstr "Tento kanál si budou moci prohlédnout pouze vybraní členové a role. msgid "Only show message previews for DMs with unread messages" msgstr "Zobrazovat náhledy zpráv pouze u soukromých konverzací s nepřečtenými zprávami" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Pouze reproduktory" @@ -24759,7 +24832,7 @@ msgstr "Otevřít podrobnosti kanálu {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Otevřít chat" @@ -24840,7 +24913,7 @@ msgstr "Otevřít emoji v prohlížeči" msgid "Open emoji picker" msgstr "Otevřít výběr emoji" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Otevřít záznam událostí" @@ -25280,6 +25353,16 @@ msgstr "Ostatní aktivní vlákna" msgid "Other devices" msgstr "Další zařízení" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Ostatní účastníci mohou vidět vaši IP adresu." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Ostatní účastníci P2P hovoru uvidí vaši IP adresu." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Vlastnictví se momentálně nepodařilo převést na uživatele <0>{use msgid "Ownership transferred" msgstr "Vlastnictví bylo převedeno" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Hovory mezi uživateli nejsou momentálně dostupné." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Jen hovory P2P" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Připnutá skupina" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Připnutá skupinová konverzace" @@ -26536,7 +26635,7 @@ msgstr "Tlačítko náhledu" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Náhled kamery" @@ -27107,7 +27206,7 @@ msgstr "Klávesa pro režim Stisknutím mluvit" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Stisknutím mluvit" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Zkraťte zprávu a zkuste to znovu." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Zkraťte téma a zkuste to znovu." @@ -31488,7 +31587,7 @@ msgstr "Zobrazit ovládací prvky hlasitosti připojení" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Zobrazit zkratky kontextové nabídky" @@ -31573,7 +31672,7 @@ msgstr "Zobrazit méně zvukových efektů" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Zobrazit obrys fokusu kolem textového pole chatu" @@ -31701,7 +31800,7 @@ msgstr "Zobrazit ztlumené kanály" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Zobrazit moji kameru" @@ -31742,7 +31841,7 @@ msgstr "Zobrazit nové příspěvky jako nepřečtené" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Zobrazit účastníky bez videa" @@ -32144,7 +32243,7 @@ msgstr "Tichý hovor" msgid "Silent calls" msgstr "Tiché hovory" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Tiché, když zachytávací zařízení nemá vlastní zvukový vstup" @@ -32655,8 +32754,8 @@ msgstr "Něco se pokazilo." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Něco se pokazilo. Zkuste to prosím za chvíli znovu." @@ -32750,7 +32849,7 @@ msgstr "Zvuky" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Zdroj" @@ -32859,7 +32958,7 @@ msgstr "Mluvte. Až budete hotovi, stiskněte Zastavit – nahrávku pak můžet #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Reproduktor" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Založit skupinovou konverzaci" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Spustit videohovor?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Zahájit hlasový hovor?" @@ -33092,6 +33191,16 @@ msgstr "Počáteční datum musí být dřívější než koncové datum." msgid "Start drag and drop" msgstr "Spustit přetažení" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Začít v režimu P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Začít v režimu P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Začít minimalizováno" msgid "Start over" msgstr "Začít znovu" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Začít P2P hovor" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Pro zúžení výsledků začněte hledání jednou z předpon <0>{peopl msgid "Start speaking" msgstr "Spustit předčítání" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Začít standardní hovor" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Stavový panel" msgid "Status:" msgstr "Stav:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Zůstat" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Zastavit" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Přestat být kanálem oznámení?" @@ -33921,6 +34045,11 @@ msgstr "Přepnout na předchozí komunitu nebo Přímé zprávy" msgid "Switch to single column" msgstr "Přepnout na jeden sloupec" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Přepnout na standardní hovor" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Skupina odepnuta" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Skupinová konverzace odepnuta" @@ -37727,6 +37856,11 @@ msgstr "Neověřený e-mail" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Až {DISCOVERY_MAX_TAGS} štítků pomůže lidem najít vaši komunitu. Zobrazují se ve vyhledávání v Objevování." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Až {maxParticipants, plural, one {# osoba} other {# osob}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Nahrávání emoji" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Odesílání videa:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Nastavení videa" @@ -39570,7 +39704,7 @@ msgstr "Nahrávání hlasu není v tomto prohlížeči podporováno." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Hlasový region" @@ -39582,7 +39716,7 @@ msgstr "Oznamovací lišta pro obnovení hlasové relace" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Nastavení hlasu" diff --git a/fluxer_app/src/features/i18n/locales/da/messages.po b/fluxer_app/src/features/i18n/locales/da/messages.po index 4d49a25b9..9977d9488 100644 --- a/fluxer_app/src/features/i18n/locales/da/messages.po +++ b/fluxer_app/src/features/i18n/locales/da/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# blokeret besked} other {# blokerede beskeder}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# kanal følger} other {# kanaler følger}} denne kanal. Ved at konvertere den til en tekstkanal fjernes disse følgere." @@ -2720,6 +2720,11 @@ msgstr "En standardemoji. Du kan bruge den overalt på {productName}." msgid "A description is required." msgstr "En beskrivelse er påkrævet." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "En direkte forbindelse kunne ikke oprettes. I kan ikke høre eller se hinanden." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "en indlejring med rigt indhold eller en linkforhåndsvisning (ikke en up msgid "A separate profile for each community" msgstr "En separat profil for hvert fællesskab" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Et almindeligt opkald går gennem en server og virker for alle." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Administratorer har sat invitationer på pause – du kan ikke deltage l #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Alle kan anmode om en konto, men en administrator skal godkende den." msgid "Anyone in my communities" msgstr "Alle i mine fællesskaber" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Alle, der ikke kan oprette direkte forbindelse, kan ikke deltage, før nogen skifter opkaldet til et standardopkald." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Alle, der finder dig, kan sende en anmodning. Slå dette fra for at vælge grupper nedenfor." @@ -5112,6 +5127,16 @@ msgstr "Lyd" msgid "Audio & video" msgstr "Lyd og video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Lyd og video sendes direkte mellem deltagere, op til {maxParticipants, plural, one {# person} other {# personer}}. Alle accepterer, før de tilslutter sig, og kan se hinandens IP-adresse." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Lyd og video sendes direkte mellem deltagerne." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Forslag til autoudfyldelse: {label}, {selectedOptionPosition} af {result #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatisk" @@ -6936,15 +6961,15 @@ msgstr "Kan ikke slette konto" msgid "Capture" msgstr "Optag" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Optag app-lyd" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Optag computerens lyd" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Optag enhedslyd" @@ -7017,7 +7042,7 @@ msgstr "Kategori-ID kopieret" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Kategorinavn" @@ -7554,7 +7579,7 @@ msgstr "Sektioner med kanaloplysninger" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Kanal-id kopieret" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "kanallink" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Kanallink kopieret" @@ -7604,7 +7629,7 @@ msgstr "Kanaladministration" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Kanal synkroniseret med overordnet kategori" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Kanalemnet er for langt." @@ -7668,8 +7693,8 @@ msgstr "Valg af kanaltype" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Kanal opdateret" @@ -9421,7 +9446,7 @@ msgstr "Bekræft, før du deltager i talekanaler" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Bekræft, før opkald startes" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Samtaler" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Konverter" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Konverterede kanalen til en meddelelseskanal" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Konvertering af denne kanal til en tekstkanal fjerner alle kanaler, der følger den." @@ -9949,7 +9974,7 @@ msgstr "Kopiér kanal-ID" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Kopiér kanallink" @@ -10379,13 +10404,18 @@ msgstr "Kunne ikke gennemføre login. Prøv igen." msgid "Couldn't connect to {domain}. Try again." msgstr "Kunne ikke oprette forbindelse til {domain}. Prøv igen." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Kunne ikke oprette forbindelse til {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Kunne ikke oprette forbindelse til taleopkald. Prøv igen." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Kunne ikke konvertere denne kanal" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Kunne ikke downloade opdateringen til computerappen" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Kunne ikke duplikere denne kanal" @@ -10511,6 +10541,11 @@ msgstr "Kunne ikke ignorere venneanmodningen. Prøv igen." msgid "Couldn't join community" msgstr "Kunne ikke deltage i fællesskabet" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Kunne ikke deltage i P2P-opkaldet. Prøv igen." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Kunne ikke åbne faktureringsportalen" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Kunne ikke fastgøre denne samtale" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Kunne ikke køre kommandoen: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Kunne ikke gemme kanalindstillinger" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Kunne ikke afmelde denne kanal" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Kunne ikke fjerne fastgørelse af denne samtale" @@ -11374,7 +11409,7 @@ msgstr "Brugerdefineret" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Brugerdefineret" @@ -11938,7 +11973,7 @@ msgstr "Standard" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Slet \"{stickerName}\"? Dette kan ikke fortrydes." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Slet {channelLabel}? Kan ikke fortrydes." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Enhedslyd ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Kun lyden fra enheden" @@ -12694,7 +12729,7 @@ msgstr "Enhedsdetaljer" msgid "Device revoked" msgstr "Enhed logget ud" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Valg af enhed" @@ -12758,6 +12793,11 @@ msgstr "Forskellige størrelser og antal medlemmer" msgid "Dim strikethrough text" msgstr "Dæmp gennemstreget tekst" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Direkte forbindelse. Medier er krypteret mellem deltagere." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Sidepanel med direkte beskeder" msgid "DNS TXT record" msgstr "DNS TXT-post" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Spørg ikke igen, accepter altid" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplikér" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplikér kanal" @@ -13825,7 +13870,7 @@ msgstr "Rediger kategori" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Rediger kanal" @@ -15004,7 +15049,7 @@ msgstr "Escape-tast" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Escape-tasten afslutter tastaturtilstand" @@ -15047,6 +15092,11 @@ msgstr "Alle communities, der følger denne kanal, får en kopi. Hvis du rediger msgid "Everyone" msgstr "Alle" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Alle i opkaldet får Plutonium-streamkvalitet gratis: op til 4K-opløsning og 60 billeder i sekundet." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Tildelte {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Giver alle tilladelser og omgår kanalbegrænsninger. Giver omfattende adgang." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Detaljeret valg" @@ -17417,7 +17467,7 @@ msgstr "Græsk" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Gittervisning" @@ -17909,7 +17959,7 @@ msgstr "Skjul deltagere" msgid "Hide password" msgstr "Skjul adgangskode" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Skjul miniaturebilledet til forhåndsvisning" @@ -18322,11 +18372,11 @@ msgstr "Ignorer" msgid "Ignore friend request" msgstr "Ignorer venneanmodning" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignorer hardwareenheder" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignorer inputmedie" @@ -18357,7 +18407,7 @@ msgstr "Ignorer venneanmodningen fra {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignorer ventetiden i langsom tilstand i denne kanal." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignorer virtuelle enheder" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Inputenhed" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Inputniveau" @@ -18865,7 +18915,7 @@ msgstr "Inputovervågning" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Inputprofil" @@ -19431,6 +19481,12 @@ msgstr "Jitter:" msgid "Join" msgstr "Deltag" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Deltag" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Bliv medlem af et fællesskab" msgid "Join a community to unlock stickers." msgstr "Bliv medlem af et fællesskab for at få adgang til klistermærker." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Deltage i et P2P-opkald?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Sådan blev du medlem" msgid "Join on this device" msgstr "Deltag på denne enhed" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Spring direkte ind i P2P-opkald" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Forlade {threadName}? Det er privat, så du kan kun komme tilbage, hvis msgid "Leave blank for a random username" msgstr "Lad feltet være tomt for et tilfældigt brugernavn" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Forlad opkald" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Lader brugerdefineret CSS vise gennemsigtige områder i vinduet efter genstart af computerappen." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Lad andre communities følge denne kanal og få kopier af det, du udgiver." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Lavt" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Lavere latenstid." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Spejl kamera" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Mit fede klistermærke" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Min kategori" @@ -24444,7 +24517,7 @@ msgstr "Fortsæt kun, hvis du startede denne login på en enhed, du kontrollerer msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Kun kreditkort accepteres. Debetkort og forudbetalte kort kan ikke bruges til adgangsbekræftelse for indhold for voksne." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Kun standardhøjttalere" @@ -24522,7 +24595,7 @@ msgstr "Kun udvalgte medlemmer og roller vil kunne se denne kanal." msgid "Only show message previews for DMs with unread messages" msgstr "Vis kun beskedforhåndsvisninger for DM'er med ulæste beskeder" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Kun højttalere" @@ -24759,7 +24832,7 @@ msgstr "Åbn kanaloplysninger for {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Åbn chat" @@ -24840,7 +24913,7 @@ msgstr "Åbn emoji i browser" msgid "Open emoji picker" msgstr "Åbn emoji-vælger" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Åbn hændelseslog" @@ -25280,6 +25353,16 @@ msgstr "Andre aktive tråde" msgid "Other devices" msgstr "Andre enheder" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Andre deltagere kan se din IP-adresse." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Andre deltagere i et peer-to-peer-opkald kan se din IP-adresse." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Ejerskabet kunne ikke overføres til <0>{username} på nuværende ti msgid "Ownership transferred" msgstr "Ejerskab overført" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "P2P-opkald er ikke tilgængelige lige nu." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Kun P2P-opkald" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Fastgjort gruppe" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Fastgjort gruppe-DM" @@ -26536,7 +26635,7 @@ msgstr "Forhåndsvisningsknap" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Forhåndsvis kamera" @@ -27107,7 +27206,7 @@ msgstr "Tast til Tryk for at tale" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Tryk for at tale" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Forkort beskeden, og prøv igen." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Forkort emnet, og prøv igen." @@ -31488,7 +31587,7 @@ msgstr "Vis lydstyrkekontroller for forbindelser" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Vis genveje til kontekstmenu" @@ -31573,7 +31672,7 @@ msgstr "Vis færre lydeffekter" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Vis fokusring på chattekstfelt" @@ -31701,7 +31800,7 @@ msgstr "Vis kanaler sat på lydløs" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Vis mit eget kamera" @@ -31742,7 +31841,7 @@ msgstr "Vis nye indlæg som ulæste" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Vis deltagere uden video" @@ -32144,7 +32243,7 @@ msgstr "Lydløst opkald" msgid "Silent calls" msgstr "Lydløse opkald" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Stille, når enheden ikke har egen lydinput" @@ -32655,8 +32754,8 @@ msgstr "Der opstod en fejl." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Der opstod en fejl. Prøv venligst igen om et øjeblik." @@ -32750,7 +32849,7 @@ msgstr "Lyde" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Kilde" @@ -32859,7 +32958,7 @@ msgstr "Tal nu. Tryk på Stop, når du er færdig – du kan trimme bagefter." #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Højttaler" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Start en gruppesamtale" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Start et videoopkald?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Start et taleopkald?" @@ -33092,6 +33191,16 @@ msgstr "Startdatoen skal være før slutdatoen." msgid "Start drag and drop" msgstr "Start træk og slip" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Start i P2P-tilstand" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Start i P2P-tilstand?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Start minimeret" msgid "Start over" msgstr "Start forfra" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Start P2P-opkald" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Start søgninger med <0>{peoplePrefix} <1>{textChannelPrefix} <2 msgid "Start speaking" msgstr "Start oplæsning" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Start standardopkald" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Statuspanel" msgid "Status:" msgstr "Status:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Bliv" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Stop" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Stop med at være en annonceringskanal?" @@ -33921,6 +34045,11 @@ msgstr "Skift til forrige fællesskab eller direkte beskeder" msgid "Switch to single column" msgstr "Skift til én kolonne" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Skift til standardopkald" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Fastgørelse af gruppe fjernet" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Fastgørelse af gruppe-DM fjernet" @@ -37727,6 +37856,11 @@ msgstr "Ikke-bekræftet e-mail" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Op til {DISCOVERY_MAX_TAGS} tags hjælper folk med at finde dit fællesskab. De vises i søgningen under Opdag." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Op til {maxParticipants, plural, one {# person} other {# personer}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Uploader emojis" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Video sendt:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Videoindstillinger" @@ -39570,7 +39704,7 @@ msgstr "Optagelse af talebeskeder understøttes ikke i denne browser." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Taleregion" @@ -39582,7 +39716,7 @@ msgstr "Beskedbjælke om gendannelse af taleopkald" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Taleindstillinger" diff --git a/fluxer_app/src/features/i18n/locales/de/messages.po b/fluxer_app/src/features/i18n/locales/de/messages.po index 3ffd958bc..f3811269c 100644 --- a/fluxer_app/src/features/i18n/locales/de/messages.po +++ b/fluxer_app/src/features/i18n/locales/de/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# blockierte Nachricht} other {# blockierte Nachrichten}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# Kanal folgt} other {# Kanäle folgen}} diesem Kanal. Die Umwandlung in einen Textkanal entfernt diese Follower." @@ -2720,6 +2720,11 @@ msgstr "Ein Standard-Emoji. Du kannst es überall auf {productName} verwenden." msgid "A description is required." msgstr "Eine Beschreibung ist erforderlich." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Eine direkte Verbindung konnte nicht hergestellt werden. Ihr könnt euch nicht hören oder sehen." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "ein formatierter eingebetteter Inhalt oder eine Linkvorschau (keine hoch msgid "A separate profile for each community" msgstr "Ein separates Profil für jede Community" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Ein normaler Anruf läuft über einen Server und funktioniert für alle." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Admins haben Einladungen pausiert – du kannst jetzt nicht beitreten." #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Jeder kann einen Account beantragen, aber ein Administrator muss den Ant msgid "Anyone in my communities" msgstr "Jeder in meinen Communities" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Wenn jemand keine direkte Verbindung herstellen kann, kann er nicht teilnehmen, bis jemand den Anruf in einen Standardanruf umwandelt." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Jeder, der dich findet, kann eine Anfrage senden. Schalte dies aus, um unten Gruppen auszuwählen." @@ -5112,6 +5127,16 @@ msgstr "Audio" msgid "Audio & video" msgstr "Audio & Video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Audio und Video gehen direkt zwischen den Teilnehmern, bis zu {maxParticipants, plural, one {# Person} other {# Personen}}. Alle stimmen vor dem Beitritt zu und können die IP-Adresse des anderen sehen." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Audio und Video werden direkt zwischen den Teilnehmern übertragen." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Vervollständigungsvorschlag: {label}, {selectedOptionPosition} von {res #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatisch" @@ -6936,15 +6961,15 @@ msgstr "Account kann nicht gelöscht werden" msgid "Capture" msgstr "Aufnehmen" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "App-Audio aufnehmen" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Desktop-Audio aufnehmen" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Geräteaudio aufnehmen" @@ -7017,7 +7042,7 @@ msgstr "Kategorie-ID kopiert" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Kategoriename" @@ -7554,7 +7579,7 @@ msgstr "Kanalinfo-Abschnitte" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Kanal-ID kopiert" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "Kanal-Link" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Kanallink kopiert" @@ -7604,7 +7629,7 @@ msgstr "Kanalverwaltung" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Kanal mit übergeordneter Kategorie synchronisiert" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Kanalthema ist zu lang." @@ -7668,8 +7693,8 @@ msgstr "Kanaltyp auswählen" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Kanal aktualisiert" @@ -9421,7 +9446,7 @@ msgstr "Vor Beitritt zu Sprachkanälen bestätigen" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Vor Anrufen bestätigen" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Unterhaltungen" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Konvertieren" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Kanal in Ankündigungskanal umgewandelt" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Wenn dieser Kanal in einen Textkanal umgewandelt wird, werden alle folgenden Kanäle entfernt." @@ -9949,7 +9974,7 @@ msgstr "Kanal-ID kopieren" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Kanallink kopieren" @@ -10379,13 +10404,18 @@ msgstr "Anmeldung konnte nicht abgeschlossen werden. Bitte versuche es noch einm msgid "Couldn't connect to {domain}. Try again." msgstr "Konnte keine Verbindung zu {domain} herstellen. Erneut versuchen." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Konnte keine Verbindung zu {names} herstellen" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Verbindung zum Sprachchat fehlgeschlagen. Bitte versuche es erneut." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Diese Nachricht konnte nicht konvertiert werden" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Desktop-Update konnte nicht heruntergeladen werden" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Kanal konnte nicht dupliziert werden" @@ -10511,6 +10541,11 @@ msgstr "Freundschaftsanfrage konnte nicht ignoriert werden. Bitte versuche es er msgid "Couldn't join community" msgstr "Der Community konnte nicht beigetreten werden" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Konnte dem P2P-Anruf nicht beitreten. Versuche es erneut." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Das Abrechnungsportal konnte nicht geöffnet werden" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Konversation konnte nicht angeheftet werden" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Befehl konnte nicht ausgeführt werden: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Konnte Kanaleinstellungen nicht speichern" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Konnte diesem Kanal nicht entfolgen" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Anheftung dieser Unterhaltung konnte nicht aufgehoben werden" @@ -11374,7 +11409,7 @@ msgstr "Benutzerdefiniert" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Benutzerdefiniert" @@ -11938,7 +11973,7 @@ msgstr "Standard" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "\"{stickerName}\" löschen? Kann nicht rückgängig gemacht werden." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "{channelLabel} löschen? Kann nicht rückgängig gemacht werden." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Geräteaudio ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Nur Audiogerät" @@ -12694,7 +12729,7 @@ msgstr "Gerätedetails" msgid "Device revoked" msgstr "Gerät entfernt" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Geräteauswahl" @@ -12758,6 +12793,11 @@ msgstr "Verschiedene Größen & Mitgliederzahlen" msgid "Dim strikethrough text" msgstr "Durchgestrichenen Text abdunkeln" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Direkte Verbindung. Medien sind zwischen den Teilnehmern verschlüsselt." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "DM-Seitenleiste" msgid "DNS TXT record" msgstr "DNS-TXT-Eintrag" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Nicht mehr fragen, immer zustimmen" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplizieren" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Kanal duplizieren" @@ -13825,7 +13870,7 @@ msgstr "Kategorie bearbeiten" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Kanal bearbeiten" @@ -15004,7 +15049,7 @@ msgstr "Escape-Taste" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Escape-Taste beendet den Tastaturmodus" @@ -15047,6 +15092,11 @@ msgstr "Jede Community, die diesem Kanal folgt, erhält eine Kopie. Wenn du sie msgid "Everyone" msgstr "Jeder" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Alle im Anruf erhalten kostenlos die Plutonium-Streamingqualität: bis zu 4K-Auflösung und 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "{permissions} gewährt" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Gewährt alle Berechtigungen und umgeht Kanalbeschränkungen. Hochsensibel." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Feinauswahl" @@ -17417,7 +17467,7 @@ msgstr "Griechisch" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Rasteransicht" @@ -17909,7 +17959,7 @@ msgstr "Teilnehmer ausblenden" msgid "Hide password" msgstr "Passwort ausblenden" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Vorschaubild ausblenden" @@ -18322,11 +18372,11 @@ msgstr "Ignorieren" msgid "Ignore friend request" msgstr "Freundschaftsanfrage ignorieren" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Hardwaregeräte ignorieren" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Eingabemedien ignorieren" @@ -18357,7 +18407,7 @@ msgstr "Freundschaftsanfrage von {displayName} ignorieren?" msgid "Ignore this channel's slowmode cooldown." msgstr "Wartezeit des Slow-Modus in diesem Kanal ignorieren." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Virtuelle ignorieren" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Eingabegerät" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Eingangspegel" @@ -18865,7 +18915,7 @@ msgstr "Eingabeüberwachung" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Eingabeprofil" @@ -19431,6 +19481,12 @@ msgstr "Jitter:" msgid "Join" msgstr "Beitreten" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Beitreten" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Einer Community beitreten" msgid "Join a community to unlock stickers." msgstr "Tritt einer Community bei, um Sticker freizuschalten." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "An einem P2P-Anruf teilnehmen?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Beitrittsmethode" msgid "Join on this device" msgstr "Auf diesem Gerät beitreten" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "An P2P-Anrufen ohne Nachfrage teilnehmen" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "{threadName} verlassen? Dies ist ein privater Thread. Du kannst nur zur msgid "Leave blank for a random username" msgstr "Für einen zufälligen Benutzernamen leer lassen" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Anruf verlassen" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Ermöglicht es benutzerdefiniertem CSS, nach dem Neustart der Desktop-App transparente Fensterbereiche anzuzeigen." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Ermöglicht anderen Communities, diesem Kanal zu folgen und Kopien von dem zu erhalten, was du veröffentlichst." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Niedrig" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Geringere Latenz." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Kamera spiegeln" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Mein toller Sticker" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Meine Kategorie" @@ -24444,7 +24517,7 @@ msgstr "Nur fortfahren, wenn du diese Anmeldung auf einem Gerät gestartet hast, msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Es werden nur Kreditkarten akzeptiert. Debit- und Prepaid-Karten können für die Altersprüfung für nicht jugendfreie Inhalte nicht verwendet werden." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Nur Standardlautsprecher" @@ -24522,7 +24595,7 @@ msgstr "Nur ausgewählte Mitglieder und Rollen können diesen Kanal sehen." msgid "Only show message previews for DMs with unread messages" msgstr "Nachrichtenvorschauen nur für DMs mit ungelesenen Nachrichten anzeigen" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Nur Lautsprecher" @@ -24759,7 +24832,7 @@ msgstr "Kanaldetails für {channelName} öffnen" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Chat öffnen" @@ -24840,7 +24913,7 @@ msgstr "Emoji im Browser öffnen" msgid "Open emoji picker" msgstr "Emoji-Auswahl öffnen" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Ereignis-Sink öffnen" @@ -25280,6 +25353,16 @@ msgstr "Andere aktive Threads" msgid "Other devices" msgstr "Andere Geräte" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Andere Teilnehmer können deine IP-Adresse sehen." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Andere Teilnehmer in einem P2P-Anruf können deine IP-Adresse sehen." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Das Eigentum konnte derzeit nicht an <0>{username} übertragen werde msgid "Ownership transferred" msgstr "Eigentumsrechte wurden übertragen" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Peer-to-Peer-Anrufe sind derzeit nicht verfügbar." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Nur P2P-Anrufe" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Angepinnte Gruppe" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Angepinnte Gruppen-DM" @@ -26536,7 +26635,7 @@ msgstr "Vorschau-Button" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Kameravorschau" @@ -27107,7 +27206,7 @@ msgstr "Push-to-Talk-Taste" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Push-to-Talk" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Nachricht kürzen und erneut versuchen." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Kürze das Thema und versuche es erneut." @@ -31488,7 +31587,7 @@ msgstr "Verbindungslautstärkeregler anzeigen" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Tastenkombinationen im Kontextmenü anzeigen" @@ -31573,7 +31672,7 @@ msgstr "Weniger Soundeffekte anzeigen" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Fokusring im Texteingabefeld anzeigen" @@ -31701,7 +31800,7 @@ msgstr "Stummgeschaltete Kanäle anzeigen" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Eigene Kamera anzeigen" @@ -31742,7 +31841,7 @@ msgstr "Neue Beiträge als ungelesen anzeigen" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Teilnehmer ohne Video anzeigen" @@ -32144,7 +32243,7 @@ msgstr "Stummer Anruf" msgid "Silent calls" msgstr "Lautlose Anrufe" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Stumm, wenn das Aufnahmegerät keinen eigenen Audioeingang hat" @@ -32655,8 +32754,8 @@ msgstr "Etwas ist schiefgelaufen." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Es ist ein Fehler aufgetreten. Bitte versuche es gleich erneut." @@ -32750,7 +32849,7 @@ msgstr "Töne" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Quelle" @@ -32859,7 +32958,7 @@ msgstr "Sprich jetzt. Drücke „Stopp“, wenn du fertig bist – du kannst es #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Lautsprecher" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Gruppen-DM starten" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Videoanruf starten?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Sprachanruf starten?" @@ -33092,6 +33191,16 @@ msgstr "Das Startdatum muss vor dem Enddatum liegen." msgid "Start drag and drop" msgstr "Ziehen und Ablegen starten" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Im P2P-Modus starten" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Im P2P-Modus starten?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Minimiert starten" msgid "Start over" msgstr "Neu beginnen" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "P2P-Anruf starten" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Beginne Suchvorgänge mit <0>{peoplePrefix} <1>{textChannelPrefix}{peoplePrefix}< msgid "Start speaking" msgstr "Έναρξη ομιλίας" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Έναρξη κανονικής κλήσης" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Πλαίσιο κατάστασης" msgid "Status:" msgstr "Κατάσταση:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Παραμονή" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Διακοπή" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Να σταματήσει να είναι κανάλι ανακοινώσεων;" @@ -33921,6 +34045,11 @@ msgstr "Εναλλαγή στην προηγούμενη κοινότητα ή msgid "Switch to single column" msgstr "Εναλλαγή σε μονή στήλη" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Μετάβαση σε τυπική κλήση" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Καταργήθηκε το καρφίτσωμα της ομάδας" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Καταργήθηκε το καρφίτσωμα της ομαδικής συνομιλίας" @@ -37727,6 +37856,11 @@ msgstr "Μη επαληθευμένο email" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Έως {DISCOVERY_MAX_TAGS} ετικέτες βοηθούν τους χρήστες να βρουν την κοινότητά σου. Εμφανίζονται στην αναζήτηση της Ανακάλυψης." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Έως {maxParticipants, plural, one {# άτομο} other {# άτομα}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Ανέβασμα emoji" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Αποστολή βίντεο:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Ρυθμίσεις βίντεο" @@ -39570,7 +39704,7 @@ msgstr "Η ηχογράφηση φωνής δεν υποστηρίζεται σ #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Περιοχή φωνής" @@ -39582,7 +39716,7 @@ msgstr "Γραμμή ειδοποιήσεων επαναφοράς φωνητι #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Ρυθμίσεις φωνής" diff --git a/fluxer_app/src/features/i18n/locales/en-GB/messages.po b/fluxer_app/src/features/i18n/locales/en-GB/messages.po index a6cdf7ba7..d22bdaa06 100644 --- a/fluxer_app/src/features/i18n/locales/en-GB/messages.po +++ b/fluxer_app/src/features/i18n/locales/en-GB/messages.po @@ -1120,7 +1120,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# blocked message} other {# blocked messages}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." @@ -2728,6 +2728,12 @@ msgstr "A default emoji. You can use it anywhere on {productName}." msgid "A description is required." msgstr "A description is required." +# auto-i18n: reviewed unchanged +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "A direct connection couldn't be made. You can't hear or see each other." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2802,6 +2808,12 @@ msgstr "a rich embed or link preview (not an uploaded file)" msgid "A separate profile for each community" msgstr "A separate profile for each community" +# auto-i18n: reviewed unchanged +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "A standard call goes through a server and works for everyone." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3686,7 +3698,7 @@ msgstr "Admins paused invites — you can't join right now." #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4641,6 +4653,12 @@ msgstr "Anyone can request an account, but an administrator must approve it." msgid "Anyone in my communities" msgstr "Anyone in my communities" +# auto-i18n: reviewed unchanged +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." + # auto-i18n: reviewed unchanged #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." @@ -5130,6 +5148,18 @@ msgstr "Audio" msgid "Audio & video" msgstr "Audio & video" +# auto-i18n: reviewed unchanged +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." + +# auto-i18n: reviewed unchanged +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Audio and video go directly between participants." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5437,7 +5467,7 @@ msgstr "Autocomplete suggestion: {label}, {selectedOptionPosition} of {resultCou #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatic" @@ -6956,15 +6986,15 @@ msgstr "Cannot delete account" msgid "Capture" msgstr "Capture" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Capture app audio" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Capture desktop audio" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Capture device audio" @@ -7037,7 +7067,7 @@ msgstr "Category ID copied" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Category name" @@ -7574,7 +7604,7 @@ msgstr "Channel details sections" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Channel ID copied" @@ -7592,7 +7622,7 @@ msgid "channel link" msgstr "channel link" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Channel link copied" @@ -7624,7 +7654,7 @@ msgstr "Channel management" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7671,7 +7701,7 @@ msgid "Channel synced with parent category" msgstr "Channel synced with parent category" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Channel topic is too long." @@ -7688,8 +7718,8 @@ msgstr "Channel type selection" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Channel updated" @@ -9450,7 +9480,7 @@ msgstr "Confirm before joining voice channels" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Confirm before starting calls" @@ -9851,7 +9881,7 @@ msgid "Conversations" msgstr "Conversations" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Convert" @@ -9871,7 +9901,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Converted the channel to an announcement channel" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Converting this channel to a text channel removes every channel that follows it." @@ -9978,7 +10008,7 @@ msgstr "Copy channel ID" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Copy channel link" @@ -10408,13 +10438,19 @@ msgstr "Couldn't complete sign-in. Try again." msgid "Couldn't connect to {domain}. Try again." msgstr "Couldn't connect to {domain}. Try again." +# auto-i18n: reviewed unchanged +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Couldn't connect to {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Couldn't connect to voice. Please try again." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Couldn't convert this channel" @@ -10511,7 +10547,7 @@ msgid "Couldn't download the desktop update" msgstr "Couldn't download the desktop update" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Couldn't duplicate this channel" @@ -10540,6 +10576,12 @@ msgstr "Couldn't ignore the friend request. Try again." msgid "Couldn't join community" msgstr "Couldn't join community" +# auto-i18n: reviewed unchanged +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Couldn't join the P2P call. Try again." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10630,7 +10672,7 @@ msgid "Couldn't open the billing portal" msgstr "Couldn't open the billing portal" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Couldn't pin this conversation" @@ -10737,7 +10779,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Couldn't run that command: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Couldn't save channel settings" @@ -10834,7 +10876,7 @@ msgid "Couldn't unfollow this channel" msgstr "Couldn't unfollow this channel" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Couldn't unpin this conversation" @@ -11404,7 +11446,7 @@ msgstr "Custom" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Custom" @@ -11968,7 +12010,7 @@ msgstr "Default" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12128,7 +12170,7 @@ msgstr "Delete \"{stickerName}\"? Can't be undone." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Delete {channelLabel}? Can't be undone." @@ -12711,7 +12753,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Device audio ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Device audio only" @@ -12725,7 +12767,7 @@ msgstr "Device details" msgid "Device revoked" msgstr "Device revoked" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Device selection" @@ -12789,6 +12831,12 @@ msgstr "Different sizes & member counts" msgid "Dim strikethrough text" msgstr "Dim strikethrough text" +# auto-i18n: reviewed unchanged +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Direct connection. Media is encrypted between participants." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13314,6 +13362,11 @@ msgstr "DM sidebar" msgid "DNS TXT record" msgstr "DNS TXT record" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Don't ask again, always agree" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13724,7 +13777,7 @@ msgstr "Duplicate" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplicate channel" @@ -13858,7 +13911,7 @@ msgstr "Edit category" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Edit channel" @@ -15037,7 +15090,7 @@ msgstr "Escape key" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Escape key exits keyboard mode" @@ -15080,6 +15133,12 @@ msgstr "Every community that follows this channel gets a copy. If you edit or de msgid "Everyone" msgstr "Everyone" +# auto-i18n: reviewed unchanged +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17417,7 +17476,7 @@ msgstr "Granted {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Grants all permissions and bypasses channel restrictions. Highly sensitive." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Granular selection" @@ -17453,7 +17512,7 @@ msgstr "Greek" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Grid view" @@ -17946,7 +18005,7 @@ msgstr "Hide participants" msgid "Hide password" msgstr "Hide password" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Hide preview thumbnail" @@ -18359,11 +18418,11 @@ msgstr "Ignore" msgid "Ignore friend request" msgstr "Ignore friend request" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignore hardware devices" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignore input media" @@ -18394,7 +18453,7 @@ msgstr "Ignore the friend request from {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignore this channel's slowmode cooldown." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignore virtual" @@ -18890,7 +18949,7 @@ msgid "Input device" msgstr "Input device" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Input level" @@ -18902,7 +18961,7 @@ msgstr "Input Monitoring" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Input profile" @@ -19468,6 +19527,13 @@ msgstr "Jitter:" msgid "Join" msgstr "Join" +# auto-i18n: reviewed unchanged +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Join" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19492,6 +19558,12 @@ msgstr "Join a community" msgid "Join a community to unlock stickers." msgstr "Join a community to unlock stickers." +# auto-i18n: reviewed unchanged +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Join a P2P call?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19530,6 +19602,12 @@ msgstr "Join method" msgid "Join on this device" msgstr "Join on this device" +# auto-i18n: reviewed unchanged +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Join P2P calls without asking" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20061,8 +20139,10 @@ msgstr "Leave {threadName}? It's private, so you can only come back if someone a msgid "Leave blank for a random username" msgstr "Leave blank for a random username" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Leave call" @@ -20239,7 +20319,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Lets custom CSS reveal transparent window regions after restarting the desktop app." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Lets other communities follow this channel and get copies of what you publish." @@ -20767,6 +20847,12 @@ msgstr "Louisiana" msgid "Low" msgstr "Low" +# auto-i18n: reviewed unchanged +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Lower latency." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22045,7 +22131,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22143,7 +22229,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Mirror camera" @@ -22865,7 +22951,7 @@ msgid "My awesome sticker" msgstr "My awesome sticker" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "My category" @@ -24488,7 +24574,7 @@ msgstr "Only continue if you started this sign-in on a device you control." msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Only default speakers" @@ -24566,7 +24652,7 @@ msgstr "Only selected members and roles will be able to view this channel." msgid "Only show message previews for DMs with unread messages" msgstr "Only show message previews for DMs with unread messages" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Only speakers" @@ -24803,7 +24889,7 @@ msgstr "Open channel details for {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Open chat" @@ -24884,7 +24970,7 @@ msgstr "Open emoji in browser" msgid "Open emoji picker" msgstr "Open emoji picker" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Open event sink" @@ -25324,6 +25410,18 @@ msgstr "Other active threads" msgid "Other devices" msgstr "Other devices" +# auto-i18n: reviewed unchanged +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Other participants can see your IP address." + +# auto-i18n: reviewed unchanged +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Other participants in a P2P call can see your IP address." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25439,6 +25537,24 @@ msgstr "Ownership could not be transferred to <0>{username} at this time." msgid "Ownership transferred" msgstr "Ownership transferred" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +# auto-i18n: reviewed unchanged +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "P2P calls are unavailable right now." + +# auto-i18n: reviewed unchanged +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "P2P calls only" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26144,7 +26260,7 @@ msgstr "Pinned group" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Pinned group DM" @@ -26584,7 +26700,7 @@ msgstr "Preview button" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Preview camera" @@ -27155,7 +27271,7 @@ msgstr "Push to talk key" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Push-to-talk" @@ -31441,7 +31557,7 @@ msgid "Shorten the message and try again." msgstr "Shorten the message and try again." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Shorten the topic and try again." @@ -31541,7 +31657,7 @@ msgstr "Show connection volume controls" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Show context menu shortcuts" @@ -31626,7 +31742,7 @@ msgstr "Show fewer sound effects" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Show focus ring on chat text area" @@ -31754,7 +31870,7 @@ msgstr "Show muted channels" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Show my own camera" @@ -31795,7 +31911,7 @@ msgstr "Show new posts as unread" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Show non-video participants" @@ -32199,7 +32315,7 @@ msgstr "Silent call" msgid "Silent calls" msgstr "Silent calls" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Silent when the capture device has no audio input of its own" @@ -32710,8 +32826,8 @@ msgstr "Something went wrong." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Something went wrong. Please try again in a moment." @@ -32805,7 +32921,7 @@ msgstr "Sounds" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Source" @@ -32914,7 +33030,7 @@ msgstr "Speak now. Press Stop when you are done — you can trim afterwards." #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Speaker" @@ -33112,12 +33228,12 @@ msgid "Start a group DM" msgstr "Start a group DM" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Start a video call?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Start a voice call?" @@ -33147,6 +33263,18 @@ msgstr "Start date must be earlier than end date." msgid "Start drag and drop" msgstr "Start drag and drop" +# auto-i18n: reviewed unchanged +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Start in P2P mode" + +# auto-i18n: reviewed unchanged +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Start in P2P mode?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33172,6 +33300,12 @@ msgstr "Start minimised" msgid "Start over" msgstr "Start over" +# auto-i18n: reviewed unchanged +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Start P2P call" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33186,6 +33320,12 @@ msgstr "Start searches with <0>{peoplePrefix} <1>{textChannelPrefix} <2> msgid "Start speaking" msgstr "Start speaking" +# auto-i18n: reviewed unchanged +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Start standard call" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33257,6 +33397,12 @@ msgstr "Status slate" msgid "Status:" msgstr "Status:" +# auto-i18n: reviewed unchanged +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Stay" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33464,7 +33610,7 @@ msgid "Stop" msgstr "Stop" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Stop being an announcement channel?" @@ -33977,6 +34123,12 @@ msgstr "Switch to previous community or DMs" msgid "Switch to single column" msgstr "Switch to single column" +# auto-i18n: reviewed unchanged +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Switch to standard call" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37628,7 +37780,7 @@ msgstr "Unpinned group" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Unpinned group DM" @@ -37787,6 +37939,12 @@ msgstr "Unverified email" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." +# auto-i18n: reviewed unchanged +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Up to {maxParticipants, plural, one {# person} other {# people}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38103,7 +38261,7 @@ msgstr "Uploading emojis" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39099,7 +39257,7 @@ msgstr "Video send:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Video settings" @@ -39634,7 +39792,7 @@ msgstr "Voice recording is not supported in this browser." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Voice region" @@ -39646,7 +39804,7 @@ msgstr "Voice session restore nagbar" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Voice settings" diff --git a/fluxer_app/src/features/i18n/locales/en-US/messages.po b/fluxer_app/src/features/i18n/locales/en-US/messages.po index 0f136da3d..369baee09 100644 --- a/fluxer_app/src/features/i18n/locales/en-US/messages.po +++ b/fluxer_app/src/features/i18n/locales/en-US/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# blocked message} other {# blocked messages}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." @@ -2720,6 +2720,11 @@ msgstr "A default emoji. You can use it anywhere on {productName}." msgid "A description is required." msgstr "A description is required." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "A direct connection couldn't be made. You can't hear or see each other." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "a rich embed or link preview (not an uploaded file)" msgid "A separate profile for each community" msgstr "A separate profile for each community" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "A standard call goes through a server and works for everyone." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Admins paused invites — you can't join right now." #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Anyone can request an account, but an administrator must approve it." msgid "Anyone in my communities" msgstr "Anyone in my communities" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Anyone who finds you can send a request. Turn this off to pick groups below." @@ -5112,6 +5127,16 @@ msgstr "Audio" msgid "Audio & video" msgstr "Audio & video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Audio and video go directly between participants." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Autocomplete suggestion: {label}, {selectedOptionPosition} of {resultCou #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatic" @@ -6936,15 +6961,15 @@ msgstr "Cannot delete account" msgid "Capture" msgstr "Capture" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Capture app audio" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Capture desktop audio" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Capture device audio" @@ -7017,7 +7042,7 @@ msgstr "Category ID copied" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Category name" @@ -7554,7 +7579,7 @@ msgstr "Channel details sections" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Channel ID copied" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "channel link" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Channel link copied" @@ -7604,7 +7629,7 @@ msgstr "Channel management" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Channel synced with parent category" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Channel topic is too long." @@ -7668,8 +7693,8 @@ msgstr "Channel type selection" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Channel updated" @@ -9421,7 +9446,7 @@ msgstr "Confirm before joining voice channels" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Confirm before starting calls" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Conversations" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Convert" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Converted the channel to an announcement channel" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Converting this channel to a text channel removes every channel that follows it." @@ -9949,7 +9974,7 @@ msgstr "Copy channel ID" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Copy channel link" @@ -10379,13 +10404,18 @@ msgstr "Couldn't complete sign-in. Try again." msgid "Couldn't connect to {domain}. Try again." msgstr "Couldn't connect to {domain}. Try again." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Couldn't connect to {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Couldn't connect to voice. Please try again." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Couldn't convert this channel" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Couldn't download the desktop update" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Couldn't duplicate this channel" @@ -10511,6 +10541,11 @@ msgstr "Couldn't ignore the friend request. Try again." msgid "Couldn't join community" msgstr "Couldn't join community" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Couldn't join the P2P call. Try again." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Couldn't open the billing portal" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Couldn't pin this conversation" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Couldn't run that command: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Couldn't save channel settings" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Couldn't unfollow this channel" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Couldn't unpin this conversation" @@ -11374,7 +11409,7 @@ msgstr "Custom" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Custom" @@ -11938,7 +11973,7 @@ msgstr "Default" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Delete \"{stickerName}\"? Can't be undone." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Delete {channelLabel}? Can't be undone." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Device audio ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Device audio only" @@ -12694,7 +12729,7 @@ msgstr "Device details" msgid "Device revoked" msgstr "Device revoked" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Device selection" @@ -12758,6 +12793,11 @@ msgstr "Different sizes & member counts" msgid "Dim strikethrough text" msgstr "Dim strikethrough text" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Direct connection. Media is encrypted between participants." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "DM sidebar" msgid "DNS TXT record" msgstr "DNS TXT record" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Do not ask again, always agree" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplicate" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplicate channel" @@ -13825,7 +13870,7 @@ msgstr "Edit category" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Edit channel" @@ -15004,7 +15049,7 @@ msgstr "Escape key" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Escape key exits keyboard mode" @@ -15047,6 +15092,11 @@ msgstr "Every community that follows this channel gets a copy. If you edit or de msgid "Everyone" msgstr "Everyone" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Granted {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Grants all permissions and bypasses channel restrictions. Highly sensitive." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Granular selection" @@ -17417,7 +17467,7 @@ msgstr "Greek" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Grid view" @@ -17909,7 +17959,7 @@ msgstr "Hide participants" msgid "Hide password" msgstr "Hide password" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Hide preview thumbnail" @@ -18322,11 +18372,11 @@ msgstr "Ignore" msgid "Ignore friend request" msgstr "Ignore friend request" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignore hardware devices" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignore input media" @@ -18357,7 +18407,7 @@ msgstr "Ignore the friend request from {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignore this channel's slowmode cooldown." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignore virtual" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Input device" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Input level" @@ -18865,7 +18915,7 @@ msgstr "Input Monitoring" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Input profile" @@ -19431,6 +19481,12 @@ msgstr "Jitter:" msgid "Join" msgstr "Join" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Join" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Join a community" msgid "Join a community to unlock stickers." msgstr "Join a community to unlock stickers." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Join a P2P call?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Join method" msgid "Join on this device" msgstr "Join on this device" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Join P2P calls without asking" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Leave {threadName}? It's private, so you can only come back if someone a msgid "Leave blank for a random username" msgstr "Leave blank for a random username" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Leave call" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Lets custom CSS reveal transparent window regions after restarting the desktop app." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Lets other communities follow this channel and get copies of what you publish." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Low" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Lower latency." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Mirror camera" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "My awesome sticker" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "My category" @@ -24444,7 +24517,7 @@ msgstr "Only continue if you started this sign-in on a device you control." msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Only default speakers" @@ -24522,7 +24595,7 @@ msgstr "Only selected members and roles will be able to view this channel." msgid "Only show message previews for DMs with unread messages" msgstr "Only show message previews for DMs with unread messages" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Only speakers" @@ -24759,7 +24832,7 @@ msgstr "Open channel details for {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Open chat" @@ -24840,7 +24913,7 @@ msgstr "Open emoji in browser" msgid "Open emoji picker" msgstr "Open emoji picker" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Open event sink" @@ -25280,6 +25353,16 @@ msgstr "Other active threads" msgid "Other devices" msgstr "Other devices" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Other participants can see your IP address." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Other participants in a P2P call can see your IP address." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,21 @@ msgstr "Ownership could not be transferred to <0>{username} at this time." msgid "Ownership transferred" msgstr "Ownership transferred" +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "P2P calls are unavailable right now." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "P2P calls only" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26194,7 @@ msgstr "Pinned group" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Pinned group DM" @@ -26536,7 +26634,7 @@ msgstr "Preview button" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Preview camera" @@ -27107,7 +27205,7 @@ msgstr "Push to talk key" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Push-to-talk" @@ -31387,7 +31485,7 @@ msgid "Shorten the message and try again." msgstr "Shorten the message and try again." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Shorten the topic and try again." @@ -31487,7 +31585,7 @@ msgstr "Show connection volume controls" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Show context menu shortcuts" @@ -31572,7 +31670,7 @@ msgstr "Show fewer sound effects" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Show focus ring on chat text area" @@ -31700,7 +31798,7 @@ msgstr "Show muted channels" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Show my own camera" @@ -31741,7 +31839,7 @@ msgstr "Show new posts as unread" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Show non-video participants" @@ -32143,7 +32241,7 @@ msgstr "Silent call" msgid "Silent calls" msgstr "Silent calls" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Silent when the capture device has no audio input of its own" @@ -32654,8 +32752,8 @@ msgstr "Something went wrong." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Something went wrong. Please try again in a moment." @@ -32749,7 +32847,7 @@ msgstr "Sounds" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Source" @@ -32858,7 +32956,7 @@ msgstr "Speak now. Press Stop when you are done — you can trim afterwards." #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Speaker" @@ -33056,12 +33154,12 @@ msgid "Start a group DM" msgstr "Start a group DM" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Start a video call?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Start a voice call?" @@ -33091,6 +33189,16 @@ msgstr "Start date must be earlier than end date." msgid "Start drag and drop" msgstr "Start drag and drop" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Start in P2P mode" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Start in P2P mode?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33116,6 +33224,11 @@ msgstr "Start minimized" msgid "Start over" msgstr "Start over" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Start P2P call" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33130,6 +33243,11 @@ msgstr "Start searches with <0>{peoplePrefix} <1>{textChannelPrefix} <2> msgid "Start speaking" msgstr "Start speaking" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Start standard call" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33201,6 +33319,11 @@ msgstr "Status slate" msgid "Status:" msgstr "Status:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Stay" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33408,7 +33531,7 @@ msgid "Stop" msgstr "Stop" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Stop being an announcement channel?" @@ -33920,6 +34043,11 @@ msgstr "Switch to previous community or DMs" msgid "Switch to single column" msgstr "Switch to single column" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Switch to standard call" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37567,7 +37695,7 @@ msgstr "Unpinned group" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Unpinned group DM" @@ -37726,6 +37854,11 @@ msgstr "Unverified email" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Up to {maxParticipants, plural, one {# person} other {# people}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38040,7 +38173,7 @@ msgstr "Uploading emojis" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39034,7 +39167,7 @@ msgstr "Video send:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Video settings" @@ -39569,7 +39702,7 @@ msgstr "Voice recording is not supported in this browser." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Voice region" @@ -39581,7 +39714,7 @@ msgstr "Voice session restore nagbar" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Voice settings" diff --git a/fluxer_app/src/features/i18n/locales/es-419/messages.po b/fluxer_app/src/features/i18n/locales/es-419/messages.po index 92db01d77..071c8469e 100644 --- a/fluxer_app/src/features/i18n/locales/es-419/messages.po +++ b/fluxer_app/src/features/i18n/locales/es-419/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# mensaje bloqueado} other {# mensajes bloqueados}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# canal lo sigue} other {# canales lo siguen}} en esta o en otras comunidades. Convertirlo a un canal de texto eliminará esos seguidores." @@ -2720,6 +2720,11 @@ msgstr "Un emoji predeterminado. Puedes usarlo en cualquier lugar de {productNam msgid "A description is required." msgstr "Se requiere una descripción." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "No se pudo establecer una conexión directa. No pueden escucharse ni verse." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "contenido incrustado o una vista previa de enlace (no un archivo subido) msgid "A separate profile for each community" msgstr "Un perfil independiente para cada comunidad" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Una llamada estándar pasa por un servidor y funciona para todos." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Los administradores pausaron las invitaciones. No puedes unirte ahora." #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Cualquiera puede solicitar una cuenta, pero un administrador debe aproba msgid "Anyone in my communities" msgstr "Cualquiera en mis comunidades" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Si alguien no puede conectarse directamente, no podrá participar hasta que alguien cambie la llamada a una estándar." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Cualquiera que te encuentre puede enviar una solicitud. Desactiva esto para elegir grupos a continuación." @@ -5112,6 +5127,16 @@ msgstr "Audio" msgid "Audio & video" msgstr "Audio y video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "El audio y video van directamente entre los participantes, hasta {maxParticipants, plural, one {# persona} other {# personas}}. Todos acuerdan antes de unirse y pueden ver la dirección IP de los demás." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "El audio y el video van directamente entre los participantes." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Sugerencia de autocompletar: {label}, {selectedOptionPosition} de {resul #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automático" @@ -6936,15 +6961,15 @@ msgstr "No se puede eliminar la cuenta" msgid "Capture" msgstr "Capturar" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Capturar audio de la app" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Capturar audio del escritorio" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Capturar audio del dispositivo" @@ -7017,7 +7042,7 @@ msgstr "ID de categoría copiado" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Nombre de la categoría" @@ -7554,7 +7579,7 @@ msgstr "Secciones de detalles del canal" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID del canal copiado" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "enlace al canal" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Enlace del canal copiado" @@ -7604,7 +7629,7 @@ msgstr "Administración de canales" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Canal sincronizado con la categoría principal" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "El tema del canal es demasiado largo." @@ -7668,8 +7693,8 @@ msgstr "Selección del tipo de canal" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Canal actualizado" @@ -9421,7 +9446,7 @@ msgstr "Confirmar antes de unirte a canales de voz" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Confirmar antes de iniciar llamadas" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Conversaciones" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Convertir" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Se convirtió el canal en un canal de anuncios" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Convertir este canal a un canal de texto eliminará todos los canales que lo siguen." @@ -9949,7 +9974,7 @@ msgstr "Copiar ID del canal" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Copiar enlace del canal" @@ -10379,13 +10404,18 @@ msgstr "No se pudo completar el inicio de sesión. Intenta de nuevo." msgid "Couldn't connect to {domain}. Try again." msgstr "No pudimos conectarnos a {domain}. Intenta de nuevo." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "No se pudo conectar con {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "No se pudo establecer la conexión de voz. Inténtalo de nuevo." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "No se pudo convertir este canal" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "No se pudo descargar la actualización de escritorio" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "No se pudo duplicar este canal" @@ -10511,6 +10541,11 @@ msgstr "No se pudo ignorar la solicitud de amistad. Intenta de nuevo." msgid "Couldn't join community" msgstr "No pudiste unirte a la comunidad" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "No se pudo unir a la llamada P2P. Intenta de nuevo." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "No se pudo abrir el portal de facturación" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "No se pudo fijar esta conversación" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "No se pudo ejecutar el comando: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "No se pudieron guardar la configuración del canal" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "No se pudo dejar de seguir este canal" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "No se pudo desfijar esta conversación" @@ -11374,7 +11409,7 @@ msgstr "Personalizado" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Personalizado" @@ -11938,7 +11973,7 @@ msgstr "Predeterminado" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "¿Eliminar \"{stickerName}\"? No se puede deshacer." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "¿Eliminar {channelLabel}? No se puede deshacer." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Audio del dispositivo ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Audio del dispositivo solamente" @@ -12694,7 +12729,7 @@ msgstr "Detalles del dispositivo" msgid "Device revoked" msgstr "Dispositivo revocado" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Selección de dispositivo" @@ -12758,6 +12793,11 @@ msgstr "Diferentes tamaños y recuentos de miembros" msgid "Dim strikethrough text" msgstr "Atenuar texto tachado" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Conexión directa. El contenido multimedia está cifrado entre los participantes." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Barra lateral de MD" msgid "DNS TXT record" msgstr "Registro DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "No preguntar de nuevo, aceptar siempre" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplicar" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplicar canal" @@ -13825,7 +13870,7 @@ msgstr "Editar categoría" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Editar canal" @@ -15004,7 +15049,7 @@ msgstr "Tecla Escape" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "La tecla Escape sale del modo teclado" @@ -15047,6 +15092,11 @@ msgstr "Cada comunidad que sigue este canal recibirá una copia. Si la editas o msgid "Everyone" msgstr "Todos" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Todos en la llamada obtienen calidad de transmisión Plutonium gratis: hasta resolución de fuente 4K y 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Se otorgaron {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Otorga todos los permisos y omite las restricciones del canal. Muy delicado." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Selección detallada" @@ -17417,7 +17467,7 @@ msgstr "Griego" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Vista de cuadrícula" @@ -17909,7 +17959,7 @@ msgstr "Ocultar participantes" msgid "Hide password" msgstr "Ocultar contraseña" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Ocultar miniatura de vista previa" @@ -18322,11 +18372,11 @@ msgstr "Ignorar" msgid "Ignore friend request" msgstr "Ignorar solicitud de amistad" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignorar dispositivos de hardware" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignorar contenido multimedia de entrada" @@ -18357,7 +18407,7 @@ msgstr "¿Ignorar la solicitud de amistad de {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignorar el tiempo de espera de modo lento de este canal." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignorar virtual" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Dispositivo de entrada" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Nivel de entrada" @@ -18865,7 +18915,7 @@ msgstr "Supervisión de entrada" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Perfil de entrada" @@ -19431,6 +19481,12 @@ msgstr "Fluctuación del retardo:" msgid "Join" msgstr "Unirse" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Unirse" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Unirse a una comunidad" msgid "Join a community to unlock stickers." msgstr "Únete a una comunidad para desbloquear stickers." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "¿Unirte a una llamada P2P?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Forma de unirse" msgid "Join on this device" msgstr "Unirse desde este dispositivo" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Unirse a llamadas P2P sin preguntar" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "¿Salir de {threadName}? Es privado, así que solo podrás volver si alg msgid "Leave blank for a random username" msgstr "Dejar en blanco para un nombre de usuario aleatorio" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Salir de la llamada" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Permite que el CSS personalizado revele regiones transparentes de la ventana después de reiniciar la aplicación de escritorio." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Permite que otras comunidades sigan este canal y reciban copias de lo que publiques." @@ -20729,6 +20797,11 @@ msgstr "Luisiana" msgid "Low" msgstr "Bajo" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Menor latencia." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Míchigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Reflejar cámara" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Mi sticker increíble" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Mi categoría" @@ -24444,7 +24517,7 @@ msgstr "Solo continúa si iniciaste esta sesión en un dispositivo que controlas msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Solo se aceptan tarjetas de crédito. Las tarjetas de débito y prepago no se pueden usar para la verificación de contenido para adultos." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Solo altavoces predeterminados" @@ -24522,7 +24595,7 @@ msgstr "Solo los miembros y roles seleccionados podrán ver este canal." msgid "Only show message previews for DMs with unread messages" msgstr "Mostrar vistas previas de mensajes solo en los MD con mensajes sin leer" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Solo altavoces" @@ -24759,7 +24832,7 @@ msgstr "Abrir detalles del canal {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Abrir chat" @@ -24840,7 +24913,7 @@ msgstr "Abrir emoji en el navegador" msgid "Open emoji picker" msgstr "Abrir selector de emojis" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Abrir receptor de eventos" @@ -25280,6 +25353,16 @@ msgstr "Otros hilos activos" msgid "Other devices" msgstr "Otros dispositivos" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Otros participantes pueden ver tu dirección IP." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Los otros participantes en una llamada P2P pueden ver tu dirección IP." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "No se pudo transferir la propiedad a <0>{username} en este momento." msgid "Ownership transferred" msgstr "Propiedad transferida" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Las llamadas P2P no están disponibles en este momento." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Llamadas P2P solamente" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Grupo fijado" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "MD de grupo fijado" @@ -26536,7 +26635,7 @@ msgstr "Botón de vista previa" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Previsualizar cámara" @@ -27107,7 +27206,7 @@ msgstr "Tecla para pulsar y hablar" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Pulsar para hablar" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Acorta el mensaje e inténtalo de nuevo." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Acorta el tema y vuelve a intentarlo." @@ -31488,7 +31587,7 @@ msgstr "Mostrar controles de volumen de conexión" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Mostrar atajos del menú contextual" @@ -31573,7 +31672,7 @@ msgstr "Mostrar menos efectos de sonido" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Mostrar contorno de foco en el área de texto del chat" @@ -31701,7 +31800,7 @@ msgstr "Mostrar canales silenciados" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Mostrar mi cámara" @@ -31742,7 +31841,7 @@ msgstr "Mostrar publicaciones nuevas como no leídas" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Mostrar participantes sin video" @@ -32144,7 +32243,7 @@ msgstr "Llamada silenciosa" msgid "Silent calls" msgstr "Llamadas silenciosas" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Silencioso cuando el dispositivo de captura no tiene entrada de audio propia" @@ -32655,8 +32754,8 @@ msgstr "Algo salió mal." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Algo salió mal. Intenta de nuevo en un momento." @@ -32750,7 +32849,7 @@ msgstr "Sonidos" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Original" @@ -32859,7 +32958,7 @@ msgstr "Habla ahora. Presiona Detener cuando termines; podrás recortar después #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Altavoz" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Iniciar un chat grupal" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "¿Iniciar una videollamada?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "¿Iniciar una llamada de voz?" @@ -33092,6 +33191,16 @@ msgstr "La fecha de inicio debe ser anterior a la fecha de finalización." msgid "Start drag and drop" msgstr "Iniciar la operación de arrastrar y soltar" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Iniciar en modo P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "¿Iniciar en modo P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Empezar minimizado" msgid "Start over" msgstr "Volver a empezar" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Iniciar llamada P2P" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Comienza las búsquedas con <0>{peoplePrefix} <1>{textChannelPrefix} msgid "Start speaking" msgstr "Iniciar locución" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Iniciar llamada estándar" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Panel de estado" msgid "Status:" msgstr "Estado:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Mantener" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Detener" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "¿Dejar de ser un canal de anuncios?" @@ -33921,6 +34045,11 @@ msgstr "Cambiar a la comunidad o MD anteriores" msgid "Switch to single column" msgstr "Cambiar a una columna" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Cambiar a llamada estándar" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Grupo desfijado" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "MD de grupo desfijado" @@ -37727,6 +37856,11 @@ msgstr "Correo electrónico sin verificar" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Hasta {DISCOVERY_MAX_TAGS} etiquetas ayudan a que la gente encuentre tu comunidad. Aparecen en la búsqueda de Descubrir." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Hasta {maxParticipants, plural, one {# persona} other {# personas}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Subiendo emojis" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Envío de video:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Configuración de video" @@ -39570,7 +39704,7 @@ msgstr "La grabación de voz no es compatible con este navegador." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Región de voz" @@ -39582,7 +39716,7 @@ msgstr "Barra de notificación de restauración de sesión de voz" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Configuración de voz" diff --git a/fluxer_app/src/features/i18n/locales/es-ES/messages.po b/fluxer_app/src/features/i18n/locales/es-ES/messages.po index c37553aab..006f21d5b 100644 --- a/fluxer_app/src/features/i18n/locales/es-ES/messages.po +++ b/fluxer_app/src/features/i18n/locales/es-ES/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# mensaje bloqueado} other {# mensajes bloqueados}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "Sigue este canal {count, plural, one {# canal} other {# canales}}. Convertirlo a un canal de texto eliminará esas suscripciones." @@ -2720,6 +2720,11 @@ msgstr "Un emoji predeterminado. Puedes usarlo en cualquier lugar de {productNam msgid "A description is required." msgstr "Se requiere una descripción." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "No se ha podido establecer una conexión directa. No podéis oíros ni veros." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "contenido incrustado o una vista previa de enlace (no un archivo subido) msgid "A separate profile for each community" msgstr "Un perfil independiente para cada comunidad" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Una llamada normal pasa por un servidor y funciona para todo el mundo." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Los administradores han pausado las invitaciones. No puedes unirte ahora #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Cualquiera puede solicitar una cuenta, pero un administrador debe aproba msgid "Anyone in my communities" msgstr "Cualquiera de mis comunidades" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Si alguien no puede conectarse directamente, no podrá participar hasta que alguien cambie la llamada a una llamada estándar." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Cualquiera que te encuentre puede enviar una solicitud. Desactiva esto para elegir grupos a continuación." @@ -5112,6 +5127,16 @@ msgstr "Audio" msgid "Audio & video" msgstr "Audio y vídeo" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "El audio y el vídeo van directamente entre los participantes, hasta {maxParticipants, plural, one {# persona} other {# personas}}. Todos están de acuerdo antes de unirse y pueden ver la dirección IP de los demás." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "El audio y el vídeo van directamente entre los participantes." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Sugerencia de autocompletar: {label}, {selectedOptionPosition} de {resul #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automático" @@ -6936,15 +6961,15 @@ msgstr "No se puede eliminar la cuenta" msgid "Capture" msgstr "Capturar" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Capturar audio de la aplicación" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Capturar audio del escritorio" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Capturar audio del dispositivo" @@ -7017,7 +7042,7 @@ msgstr "ID de categoría copiado" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Nombre de la categoría" @@ -7554,7 +7579,7 @@ msgstr "Secciones de detalles del canal" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID del canal copiado" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "enlace del canal" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Enlace del canal copiado" @@ -7604,7 +7629,7 @@ msgstr "Gestión de canales" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Canal sincronizado con la categoría principal" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "El tema del canal es demasiado largo." @@ -7668,8 +7693,8 @@ msgstr "Selección del tipo de canal" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Canal actualizado" @@ -9421,7 +9446,7 @@ msgstr "Confirmar antes de unirte a canales de voz" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Confirmar antes de iniciar llamadas" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Conversaciones" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Convertir" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Se ha convertido el canal en un canal de anuncios" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Al convertir este canal en un canal de texto, se eliminarán todos los canales que lo sigan." @@ -9949,7 +9974,7 @@ msgstr "Copiar ID del canal" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Copiar enlace del canal" @@ -10379,13 +10404,18 @@ msgstr "No se pudo completar el inicio de sesión. Inténtalo de nuevo." msgid "Couldn't connect to {domain}. Try again." msgstr "No se ha podido conectar con {domain}. Inténtalo de nuevo." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "No se ha podido conectar con {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "No se pudo establecer la conexión de voz. Inténtalo de nuevo." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "No se ha podido convertir este canal" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "No se ha podido descargar la actualización de escritorio" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "No se ha podido duplicar este canal" @@ -10511,6 +10541,11 @@ msgstr "No se ha podido ignorar la solicitud de amistad. Inténtalo de nuevo." msgid "Couldn't join community" msgstr "No has podido unirte a la comunidad" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "No se pudo unir a la llamada P2P. Inténtalo de nuevo." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "No se pudo abrir el portal de facturación" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "No se pudo fijar esta conversación" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "No se pudo ejecutar el comando: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "No se pudieron guardar los ajustes del canal" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "No se ha podido dejar de seguir este canal" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "No se pudo desfijar esta conversación" @@ -11374,7 +11409,7 @@ msgstr "Fuente personalizada" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Personalizado" @@ -11938,7 +11973,7 @@ msgstr "Predeterminado" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "¿Eliminar \"{stickerName}\"? No se podrá deshacer." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "¿Eliminar {channelLabel}? No se podrá deshacer." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Audio del dispositivo ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Solo audio del dispositivo" @@ -12694,7 +12729,7 @@ msgstr "Detalles del dispositivo" msgid "Device revoked" msgstr "Dispositivo revocado" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Selección de dispositivo" @@ -12758,6 +12793,11 @@ msgstr "Diferentes tamaños y número de miembros" msgid "Dim strikethrough text" msgstr "Atenuar texto tachado" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Conexión directa. Los medios se cifran entre los participantes." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Barra lateral de MD" msgid "DNS TXT record" msgstr "Registro DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "No volver a preguntar, aceptar siempre" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplicar" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplicar canal" @@ -13825,7 +13870,7 @@ msgstr "Editar categoría" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Editar canal" @@ -15004,7 +15049,7 @@ msgstr "Tecla Escape" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "La tecla Escape sale del modo teclado" @@ -15047,6 +15092,11 @@ msgstr "Cada comunidad que siga este canal recibirá una copia. Si la editas o l msgid "Everyone" msgstr "Todos" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Todos en la llamada obtienen calidad de transmisión Plutonium gratis: hasta resolución de origen 4K y 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Concedido {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Concede todos los permisos y omite las restricciones del canal. Muy sensible." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Selección granular" @@ -17417,7 +17467,7 @@ msgstr "Griego" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Vista de cuadrícula" @@ -17909,7 +17959,7 @@ msgstr "Ocultar participantes" msgid "Hide password" msgstr "Ocultar contraseña" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Ocultar miniatura de previsualización" @@ -18322,11 +18372,11 @@ msgstr "Ignorar" msgid "Ignore friend request" msgstr "Ignorar solicitud de amistad" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignorar dispositivos de hardware" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignorar contenido multimedia de entrada" @@ -18357,7 +18407,7 @@ msgstr "¿Ignorar la solicitud de amistad de {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignorar el tiempo de espera de modo lento de este canal." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignorar virtual" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Dispositivo de entrada" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Nivel de entrada" @@ -18865,7 +18915,7 @@ msgstr "Supervisión de entrada" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Perfil de entrada" @@ -19431,6 +19481,12 @@ msgstr "Fluctuación del retardo:" msgid "Join" msgstr "Unirse" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Unirse" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Unirse a una comunidad" msgid "Join a community to unlock stickers." msgstr "Únete a una comunidad para desbloquear pegatinas." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "¿Unirte a una llamada P2P?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Forma de unirse" msgid "Join on this device" msgstr "Unirse desde este dispositivo" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Unirse a llamadas P2P sin preguntar" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "¿Salir de {threadName}? Es privado, así que solo podrás volver si alg msgid "Leave blank for a random username" msgstr "Dejar en blanco para un nombre de usuario aleatorio" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Salir de la llamada" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Permite que el CSS personalizado revele regiones transparentes de la ventana después de reiniciar la aplicación de escritorio." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Permite que otras comunidades sigan este canal y reciban copias de lo que publiques." @@ -20729,6 +20797,11 @@ msgstr "Luisiana" msgid "Low" msgstr "Bajo" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Menor latencia." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Míchigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Reflejar cámara" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Mi pegatina increíble" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Mi categoría" @@ -24444,7 +24517,7 @@ msgstr "Solo continúa si iniciaste esta sesión en un dispositivo que controlas msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Solo se aceptan tarjetas de crédito. Las tarjetas de débito y prepago no se pueden usar para la comprobación de contenido para adultos." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Solo altavoces predeterminados" @@ -24522,7 +24595,7 @@ msgstr "Solo los miembros y roles seleccionados podrán ver este canal." msgid "Only show message previews for DMs with unread messages" msgstr "Mostrar vistas previas de mensajes solo en los MD con mensajes sin leer" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Solo altavoces" @@ -24759,7 +24832,7 @@ msgstr "Abrir detalles del canal {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Abrir chat" @@ -24840,7 +24913,7 @@ msgstr "Abrir emoji en el navegador" msgid "Open emoji picker" msgstr "Abrir selector de emojis" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Abrir receptor de eventos" @@ -25280,6 +25353,16 @@ msgstr "Otras conversaciones activas" msgid "Other devices" msgstr "Otros dispositivos" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Otros participantes pueden ver tu dirección IP." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Los otros participantes en una llamada P2P pueden ver tu dirección IP." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "No se pudo transferir la propiedad a <0>{username} en este momento." msgid "Ownership transferred" msgstr "Propiedad transferida" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Las llamadas P2P no están disponibles ahora mismo." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Solo llamadas P2P" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Grupo fijado" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "MD de grupo fijado" @@ -26536,7 +26635,7 @@ msgstr "Botón de vista previa" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Previsualizar cámara" @@ -27107,7 +27206,7 @@ msgstr "Tecla para pulsar y hablar" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Pulsar para hablar" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Acorta el mensaje y vuelve a intentarlo." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Acorta el tema y vuelve a intentarlo." @@ -31488,7 +31587,7 @@ msgstr "Mostrar controles de volumen de conexión" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Mostrar atajos del menú contextual" @@ -31573,7 +31672,7 @@ msgstr "Mostrar menos efectos de sonido" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Mostrar contorno de foco en el área de texto del chat" @@ -31701,7 +31800,7 @@ msgstr "Mostrar canales silenciados" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Mostrar mi propia cámara" @@ -31742,7 +31841,7 @@ msgstr "Mostrar publicaciones nuevas como no leídas" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Mostrar participantes sin vídeo" @@ -32144,7 +32243,7 @@ msgstr "Llamada silenciosa" msgid "Silent calls" msgstr "Llamadas silenciosas" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Silencioso cuando el dispositivo de captura no tiene entrada de audio propia" @@ -32655,8 +32754,8 @@ msgstr "Algo salió mal." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Ha ocurrido un error. Por favor, inténtalo de nuevo en unos instantes." @@ -32750,7 +32849,7 @@ msgstr "Sonidos" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Original" @@ -32859,7 +32958,7 @@ msgstr "Habla ahora. Pulsa Detener cuando hayas terminado; podrás recortar desp #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Altavoz" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Iniciar un MD de grupo" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "¿Iniciar una videollamada?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "¿Iniciar una llamada de voz?" @@ -33092,6 +33191,16 @@ msgstr "La fecha de inicio debe ser anterior a la fecha de finalización." msgid "Start drag and drop" msgstr "Iniciar la operación de arrastrar y soltar" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Iniciar en modo P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "¿Iniciar en modo P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Iniciar minimizado" msgid "Start over" msgstr "Empezar de nuevo" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Iniciar llamada P2P" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Comienza las búsquedas con <0>{peoplePrefix} <1>{textChannelPrefix} msgid "Start speaking" msgstr "Iniciar lectura en voz alta" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Iniciar llamada estándar" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Panel de estado" msgid "Status:" msgstr "Estado:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Mantener" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Detener" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "¿Dejar de ser un canal de anuncios?" @@ -33921,6 +34045,11 @@ msgstr "Cambiar a la comunidad o MD anteriores" msgid "Switch to single column" msgstr "Cambiar a una columna" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Cambiar a llamada estándar" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Grupo desfijado" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "MD de grupo desfijado" @@ -37727,6 +37856,11 @@ msgstr "Correo electrónico sin verificar" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Hasta {DISCOVERY_MAX_TAGS} etiquetas ayudan a la gente a encontrar tu comunidad. Aparecen en la búsqueda de Descubrir." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Hasta {maxParticipants, plural, one {# persona} other {# personas}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Subiendo emojis" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Envío de vídeo:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Ajustes de vídeo" @@ -39570,7 +39704,7 @@ msgstr "La grabación de voz no es compatible con este navegador." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Región de voz" @@ -39582,7 +39716,7 @@ msgstr "Barra de notificación de restauración de sesión de voz" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Ajustes de voz" diff --git a/fluxer_app/src/features/i18n/locales/fi/messages.po b/fluxer_app/src/features/i18n/locales/fi/messages.po index 5944570f0..8a41bd0c8 100644 --- a/fluxer_app/src/features/i18n/locales/fi/messages.po +++ b/fluxer_app/src/features/i18n/locales/fi/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# estetty viesti} other {# estettyä viestiä}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# kanava seuraa} other {# kanavaa seuraa}} tämän kanavan julkaisuja. Muuntaminen tekstikanavaksi poistaa nämä seurannat." @@ -2720,6 +2720,11 @@ msgstr "Oletusemoji. Voit käyttää sitä missä tahansa {productName}-palvelus msgid "A description is required." msgstr "Kuvaus tarvitaan." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Suoraa yhteyttä ei voitu muodostaa. Ette voi kuulla tai nähdä toisianne." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "rikastettu upotus tai linkin esikatselu (ei ladattua tiedostoa)" msgid "A separate profile for each community" msgstr "Erillinen profiili jokaiselle yhteisölle" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Tavallinen puhelu kulkee palvelimen kautta ja toimii kaikilla." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Ylläpitäjät ovat keskeyttäneet kutsut, joten et voi liittyä juuri n #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Kuka tahansa voi pyytää tiliä, mutta ylläpitäjän on hyväksyttäv msgid "Anyone in my communities" msgstr "Kuka tahansa yhteisöissäni" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Kaikki, jotka eivät voi yhdistää suoraan, eivät voi osallistua, ennen kuin joku vaihtaa puhelun tavalliseen puheluun." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Kuka tahansa sinut löytävä voi lähettää pyynnön. Poista tämä käytöstä valitaksesi ryhmät alta." @@ -5112,6 +5127,16 @@ msgstr "Ääni" msgid "Audio & video" msgstr "Ääni ja video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Ääni ja kuva kulkevat suoraan osallistujien välillä, enintään {maxParticipants, plural, one {# henkilö} other {# henkilöä}}. Kaikki hyväksyvät ennen liittymistä ja näkevät toistensa IP-osoitteet." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Ääni ja kuva kulkevat suoraan osallistujien välillä." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Automaattinen täydennysehdotus: {label}, {selectedOptionPosition}/{resu #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automaattinen" @@ -6936,15 +6961,15 @@ msgstr "Tiliä ei voi poistaa" msgid "Capture" msgstr "Kaappaa" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Kaappaa sovelluksen ääni" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Kaappaa työpöydän ääni" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Kaappaa laitteen ääni" @@ -7017,7 +7042,7 @@ msgstr "Kategorian tunnus kopioitu" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Kategorian nimi" @@ -7554,7 +7579,7 @@ msgstr "Kanavan tiedot -osiot" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Kanavan tunnus kopioitu" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "kanavalinkki" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Kanavan linkki kopioitu" @@ -7604,7 +7629,7 @@ msgstr "Kanavien hallinta" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Kanava synkronoitu yläkategorian kanssa" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Kanavan aihe on liian pitkä." @@ -7668,8 +7693,8 @@ msgstr "Kanavan tyypin valinta" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Kanava päivitetty" @@ -9421,7 +9446,7 @@ msgstr "Vahvista ennen äänikanaville liittymistä" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Vahvista ennen puhelun aloittamista" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Keskustelut" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Muunna" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Muutettu kanavaksi ilmoituskanava" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Tämän kanavan muuttaminen tekstikanavaksi poistaa kaikki sitä seuraavat kanavat." @@ -9949,7 +9974,7 @@ msgstr "Kopioi kanavan tunnus" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Kopioi kanavalinkki" @@ -10379,13 +10404,18 @@ msgstr "Kirjautuminen epäonnistui. Yritä uudelleen." msgid "Couldn't connect to {domain}. Try again." msgstr "Yhteyttä kohteeseen {domain} ei voitu muodostaa. Yritä uudelleen." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Yhteyttä {names} ei voitu muodostaa" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Ääniyhteyttä ei voitu muodostaa. Yritä uudelleen." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Kanavan muuntaminen epäonnistui" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Työpöytäsovelluksen päivitystä ei voitu ladata" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Kanavaa ei voitu monistaa" @@ -10511,6 +10541,11 @@ msgstr "Kaveripyyntöä ei voitu ohittaa. Yritä uudelleen." msgid "Couldn't join community" msgstr "Yhteisöön liittyminen epäonnistui" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Ei voitu liittyä P2P-puheluun. Yritä uudelleen." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Laskutusportaalia ei voitu avata" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Keskustelun kiinnitys epäonnistui" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Komentoa ei voitu suorittaa: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Kanavan asetuksia ei voitu tallentaa" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Kanavan seuraamisen lopettaminen epäonnistui" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Keskustelun kiinnityksen poistaminen epäonnistui" @@ -11374,7 +11409,7 @@ msgstr "Mukautettu" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Mukautettu" @@ -11938,7 +11973,7 @@ msgstr "Oletus" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Poistetaanko \"{stickerName}\"? Toimintoa ei voi kumota." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Poistetaanko {channelLabel}? Toimintoa ei voi kumota." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Laitteen ääni ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Laitteen ääni" @@ -12694,7 +12729,7 @@ msgstr "Laitteen tiedot" msgid "Device revoked" msgstr "Laite poistettu" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Laitteen valinta" @@ -12758,6 +12793,11 @@ msgstr "Eri kokoja ja jäsenmääriä" msgid "Dim strikethrough text" msgstr "Himmennä yliviivattua tekstiä" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Suora yhteys. Media salataan osallistujien välillä." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Yksityisviestien sivupalkki" msgid "DNS TXT record" msgstr "DNS TXT -tietue" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Älä kysy uudelleen, hyväksy aina" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Monista" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Monista kanava" @@ -13825,7 +13870,7 @@ msgstr "Muokkaa kategoriaa" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Muokkaa kanavaa" @@ -15004,7 +15049,7 @@ msgstr "Esc-näppäin" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Esc-näppäin sulkee näppäimistötilan" @@ -15047,6 +15092,11 @@ msgstr "Jokainen tätä kanavaa seuraava yhteisö saa kopion. Jos muokkaat tai p msgid "Everyone" msgstr "Kaikki" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Kaikki puhelussa olevat saavat Plutonium-striimin laadun ilmaiseksi: jopa 4K-lähderesoluutio ja 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Myönnetty {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Myöntää kaikki käyttöoikeudet ja ohittaa kanavarajoitukset. Erittäin arkaluontoinen." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Tarkka valinta" @@ -17417,7 +17467,7 @@ msgstr "kreikka" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Ruudukkonäkymä" @@ -17909,7 +17959,7 @@ msgstr "Piilota osallistujat" msgid "Hide password" msgstr "Piilota salasana" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Piilota esikatselukuva" @@ -18322,11 +18372,11 @@ msgstr "Ohita" msgid "Ignore friend request" msgstr "Ohita kaveripyyntö" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ohita fyysiset laitteet" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ohita syötemedia" @@ -18357,7 +18407,7 @@ msgstr "Ohita kaveripyyntö käyttäjältä {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ohita tämän kanavan hitaan tilan odotusaika." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ohita virtuaaliset" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Syöttölaite" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Syötteen taso" @@ -18865,7 +18915,7 @@ msgstr "Syötteen valvonta" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Syöttöprofiili" @@ -19431,6 +19481,12 @@ msgstr "Viiveen vaihtelu:" msgid "Join" msgstr "Liity" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Liity" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Liity yhteisöön" msgid "Join a community to unlock stickers." msgstr "Liity yhteisöön, niin saat tarrat käyttöösi." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Liity P2P-puheluun?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Liittymistapa" msgid "Join on this device" msgstr "Liity tällä laitteella" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Liity P2P-puheluihin kysymättä" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Poistutaanko yksityisestä säikeestä {threadName}? Voit palata vain, j msgid "Leave blank for a random username" msgstr "Jätä tyhjäksi, jos haluat satunnaisen käyttäjänimen" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Poistu puhelusta" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Mukautetun CSS:n avulla työpöytäsovelluksen ikkunan alueet voivat olla läpinäkyviä uudelleenkäynnistyksen jälkeen." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Sallii muiden yhteisöjen seurata tätä kanavaa ja saada kopioita julkaisemistasi viesteistä." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Matala" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Pienempi viive." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Peilaa kamera" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Mahtava tarrani" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Oma kategoria" @@ -24444,7 +24517,7 @@ msgstr "Jatka vain, jos aloitit tämän kirjautumisen laitteella, jota hallitset msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Vain luottokortit hyväksytään. Pankki- ja prepaid-kortteja ei voi käyttää aikuisille tarkoitetun sisällön tarkistukseen." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Vain oletuskaiuttimet" @@ -24522,7 +24595,7 @@ msgstr "Vain valitut jäsenet ja roolit voivat nähdä tämän kanavan." msgid "Only show message previews for DMs with unread messages" msgstr "Näytä viestien esikatselut vain yksityisviesteissä, joissa on lukemattomia viestejä" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Vain kaiuttimet" @@ -24759,7 +24832,7 @@ msgstr "Avaa kanavan {channelName} tiedot" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Avaa chat" @@ -24840,7 +24913,7 @@ msgstr "Avaa emoji selaimessa" msgid "Open emoji picker" msgstr "Avaa emojivalitsin" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Avaa tapahtumaloki" @@ -25280,6 +25353,16 @@ msgstr "Muut aktiiviset säikeet" msgid "Other devices" msgstr "Muut laitteet" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Muut osallistujat voivat nähdä IP-osoitteesi." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Muut P2P-puhelun osallistujat voivat nähdä IP-osoitteesi." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Omistajuutta ei voitu siirtää käyttäjälle <0>{username} tällä msgid "Ownership transferred" msgstr "Omistajuus siirretty" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "P2P-puhelut eivät ole käytettävissä juuri nyt." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Vain P2P-puhelut" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Kiinnitetty ryhmä" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Kiinnitetty ryhmäkeskustelu" @@ -26536,7 +26635,7 @@ msgstr "Esikatselupainike" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Esikatsele kameraa" @@ -27107,7 +27206,7 @@ msgstr "Puhu painamalla -näppäin" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Push-to-talk" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Lyhennä viestiä ja yritä uudelleen." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Lyhennä aihetta ja yritä uudelleen." @@ -31488,7 +31587,7 @@ msgstr "Näytä yhteyden äänenvoimakkuuden säätimet" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Näytä pikavalikon pikanäppäimet" @@ -31573,7 +31672,7 @@ msgstr "Näytä vähemmän äänitehosteita" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Näytä kohdistuskehys chat-tekstikentässä" @@ -31701,7 +31800,7 @@ msgstr "Näytä mykistetyt kanavat" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Näytä oma kamerani" @@ -31742,7 +31841,7 @@ msgstr "Merkitse uudet viestit lukemattomiksi" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Näytä osallistujat ilman videota" @@ -32144,7 +32243,7 @@ msgstr "Äänetön puhelu" msgid "Silent calls" msgstr "Äänettömät puhelut" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Mykistää, kun tallennuslaitteessa ei ole omaa äänisyötettä" @@ -32655,8 +32754,8 @@ msgstr "Jokin meni pieleen." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Jokin meni pieleen. Yritä uudelleen hetken kuluttua." @@ -32750,7 +32849,7 @@ msgstr "Äänet" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Alkuperäinen" @@ -32859,7 +32958,7 @@ msgstr "Puhu nyt. Paina Pysäytä, kun olet valmis – voit rajata myöhemmin." #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Kaiutin" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Aloita ryhmäkeskustelu" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Aloitetaanko videopuhelu?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Aloitetaanko äänipuhelu?" @@ -33092,6 +33191,16 @@ msgstr "Alkamispäivän on oltava ennen päättymispäivää." msgid "Start drag and drop" msgstr "Aloita vedä ja pudota -toiminto" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Aloita P2P-tilassa" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Aloitetaanko P2P-tilassa?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Aloita minimoituna" msgid "Start over" msgstr "Aloita alusta" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Aloita P2P-puhelu" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Aloita haut käyttämällä <0>{peoplePrefix}, <1>{textChannelPrefix msgid "Start speaking" msgstr "Aloita puhuminen" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Aloita tavallinen puhelu" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Tilapaneeli" msgid "Status:" msgstr "Tila:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Jatka" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Pysäytä" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Lopetetaanko ilmoituskanavana toimiminen?" @@ -33921,6 +34045,11 @@ msgstr "Vaihda edelliseen yhteisöön tai yksityisviesteihin" msgid "Switch to single column" msgstr "Vaihda yksipalstaiseen näkymään" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Vaihda tavalliseen puheluun" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Ryhmän kiinnitys poistettu" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Ryhmäkeskustelun kiinnitys poistettu" @@ -37727,6 +37856,11 @@ msgstr "Vahvistamaton sähköposti" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Enintään {DISCOVERY_MAX_TAGS} tunnistetta auttaa ihmisiä löytämään yhteisösi. Ne näkyvät Löydä-haussa." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Enintään {maxParticipants, plural, one {# henkilö} other {# henkilöä}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Ladataan emojeita" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Videon lähetys:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Videoasetukset" @@ -39570,7 +39704,7 @@ msgstr "Tämä selain ei tue ääniviestien tallennusta." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Puhealue" @@ -39582,7 +39716,7 @@ msgstr "Äänipuhelun palautusilmoituspalkki" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Ääniasetukset" diff --git a/fluxer_app/src/features/i18n/locales/fr/messages.po b/fluxer_app/src/features/i18n/locales/fr/messages.po index e8ac48f68..ab8983ff9 100644 --- a/fluxer_app/src/features/i18n/locales/fr/messages.po +++ b/fluxer_app/src/features/i18n/locales/fr/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# message bloqué} other {# messages bloqués}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# canal suit} other {# canaux suivent}} ce canal. Le convertir en canal texte supprimera ces suivis." @@ -2721,6 +2721,11 @@ msgstr "Un emoji par défaut. Vous pouvez l'utiliser n'importe où sur {productN msgid "A description is required." msgstr "Une description est requise." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Une connexion directe n'a pas pu être établie. Vous ne pouvez pas vous entendre ni vous voir." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2795,6 +2800,11 @@ msgstr "un contenu intégré enrichi ou un aperçu de lien (pas un fichier envoy msgid "A separate profile for each community" msgstr "Un profil distinct pour chaque communauté" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Un appel standard passe par un serveur et fonctionne pour tout le monde." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3677,7 +3687,7 @@ msgstr "Les administrateurs ont suspendu les invitations. Vous ne pouvez pas rej #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4629,6 +4639,11 @@ msgstr "Tout le monde peut demander un compte, mais un administrateur doit l'app msgid "Anyone in my communities" msgstr "Toute personne de mes communautés" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Toute personne ne pouvant pas se connecter directement ne pourra pas participer tant que quelqu'un n'aura pas basculé l'appel vers un appel standard." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Toute personne qui vous trouve peut envoyer une demande. Désactivez cette option pour choisir les groupes ci-dessous." @@ -5113,6 +5128,16 @@ msgstr "Audio" msgid "Audio & video" msgstr "Audio et vidéo" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "L’audio et la vidéo transitent directement entre les participants, jusqu’à {maxParticipants, plural, one {# personne} other {# personnes}}. Tout le monde donne son accord avant de rejoindre et peut voir l’adresse IP de chacun." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "L'audio et la vidéo transitent directement entre les participants." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5420,7 +5445,7 @@ msgstr "Suggestion de saisie semi-automatique : {label}, {selectedOptionPosition #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatique" @@ -6937,15 +6962,15 @@ msgstr "Impossible de supprimer le compte" msgid "Capture" msgstr "Capture" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Capturer le son de l’application" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Capturer le son de l’ordinateur" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Capturer l'audio de l'appareil" @@ -7018,7 +7043,7 @@ msgstr "ID de catégorie copié" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Nom de la catégorie" @@ -7555,7 +7580,7 @@ msgstr "Sections des détails du salon" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID du salon copié" @@ -7573,7 +7598,7 @@ msgid "channel link" msgstr "lien de salon" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Lien du salon copié" @@ -7605,7 +7630,7 @@ msgstr "Gestion des salons" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7652,7 +7677,7 @@ msgid "Channel synced with parent category" msgstr "Salon synchronisé avec la catégorie parente" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Le sujet du salon est trop long." @@ -7669,8 +7694,8 @@ msgstr "Sélection du type de salon" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Salon mis à jour" @@ -9422,7 +9447,7 @@ msgstr "Confirmer avant de rejoindre les salons vocaux" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Confirmer avant de lancer des appels" @@ -9823,7 +9848,7 @@ msgid "Conversations" msgstr "Conversations" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Convertir" @@ -9843,7 +9868,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Converti le salon en salon d'annonces" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "La conversion de cette chaîne en chaîne de texte supprimera toutes les chaînes qui la suivent." @@ -9950,7 +9975,7 @@ msgstr "Copier l'identifiant du salon" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Copier le lien du salon" @@ -10380,13 +10405,18 @@ msgstr "Impossible de terminer la connexion. Veuillez réessayer." msgid "Couldn't connect to {domain}. Try again." msgstr "Impossible de se connecter à {domain}. Réessayez." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Impossible de se connecter à {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Impossible d’établir la connexion vocale. Veuillez réessayer." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Impossible de convertir ce salon" @@ -10483,7 +10513,7 @@ msgid "Couldn't download the desktop update" msgstr "Impossible de télécharger la mise à jour de l’application de bureau" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Impossible de dupliquer ce salon" @@ -10512,6 +10542,11 @@ msgstr "Impossible d'ignorer la demande d'ami. Veuillez réessayer." msgid "Couldn't join community" msgstr "Impossible de rejoindre la communauté" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Impossible de rejoindre l'appel P2P. Réessayez." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10602,7 +10637,7 @@ msgid "Couldn't open the billing portal" msgstr "Impossible d'ouvrir le portail de facturation" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Impossible d’épingler cette conversation" @@ -10709,7 +10744,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Impossible d'exécuter cette commande : {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Impossible de sauvegarder les paramètres du canal" @@ -10806,7 +10841,7 @@ msgid "Couldn't unfollow this channel" msgstr "Impossible de ne plus suivre ce canal" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Impossible de désépingler cette conversation" @@ -11375,7 +11410,7 @@ msgstr "Personnalisé" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Personnalisé" @@ -11939,7 +11974,7 @@ msgstr "Par défaut" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12099,7 +12134,7 @@ msgstr "Supprimer « {stickerName} » ? Cette action est irréversible." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Supprimer {channelLabel} ? Cette action est irréversible." @@ -12681,7 +12716,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Audio de l'appareil ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Audio de l'appareil uniquement" @@ -12695,7 +12730,7 @@ msgstr "Détails de l’appareil" msgid "Device revoked" msgstr "Appareil révoqué" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Sélection de l'appareil" @@ -12759,6 +12794,11 @@ msgstr "Différentes tailles et nombres de membres" msgid "Dim strikethrough text" msgstr "Atténuer le texte barré" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Connexion directe. Les médias sont chiffrés entre les participants." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13283,6 +13323,11 @@ msgstr "Barre latérale des MP" msgid "DNS TXT record" msgstr "Enregistrement DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Ne plus demander, accepter toujours" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13692,7 +13737,7 @@ msgstr "Dupliquer" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Dupliquer le salon" @@ -13826,7 +13871,7 @@ msgstr "Modifier la catégorie" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Modifier le salon" @@ -15005,7 +15050,7 @@ msgstr "Touche Échap" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "La touche Échap quitte le mode clavier" @@ -15048,6 +15093,11 @@ msgstr "Chaque communauté qui suit ce canal en recevra une copie. Si vous la mo msgid "Everyone" msgstr "Tout le monde" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Tous les participants à l'appel bénéficient gratuitement de la qualité de streaming Plutonium : jusqu'à 4K en résolution source et 60 ips." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17382,7 +17432,7 @@ msgstr "A accordé {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Accorde toutes les permissions et contourne les restrictions des salons. Permission très sensible." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Sélection granulaire" @@ -17418,7 +17468,7 @@ msgstr "Grec" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Vue grille" @@ -17910,7 +17960,7 @@ msgstr "Masquer les participants" msgid "Hide password" msgstr "Masquer le mot de passe" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Masquer la miniature d’aperçu" @@ -18323,11 +18373,11 @@ msgstr "Ignorer" msgid "Ignore friend request" msgstr "Ignorer la demande d'ami" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignorer les appareils matériels" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignorer les médias entrants" @@ -18358,7 +18408,7 @@ msgstr "Ignorer la demande d'ami de {displayName} ?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignorer le délai du mode lent de ce salon." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignorer le virtuel" @@ -18854,7 +18904,7 @@ msgid "Input device" msgstr "Périphérique d'entrée" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Niveau d'entrée" @@ -18866,7 +18916,7 @@ msgstr "Surveillance des entrées" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Profil d'entrée" @@ -19432,6 +19482,12 @@ msgstr "Gigue :" msgid "Join" msgstr "Rejoindre" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Rejoindre" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19456,6 +19512,11 @@ msgstr "Rejoindre une communauté" msgid "Join a community to unlock stickers." msgstr "Rejoignez une communauté pour débloquer des stickers." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Rejoindre un appel P2P ?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19494,6 +19555,11 @@ msgstr "Méthode d'adhésion" msgid "Join on this device" msgstr "Rejoindre sur cet appareil" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Rejoindre les appels P2P sans demander" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20024,8 +20090,10 @@ msgstr "Quitter {threadName} ? Il est privé, vous ne pourrez donc y revenir que msgid "Leave blank for a random username" msgstr "Laisser vide pour un nom d'utilisateur aléatoire" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Quitter l'appel" @@ -20202,7 +20270,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Permet au CSS personnalisé de révéler les zones de fenêtre transparentes après le redémarrage de l'application de bureau." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Permet à d'autres communautés de suivre ce canal et de recevoir des copies de ce que vous publiez." @@ -20730,6 +20798,11 @@ msgstr "Louisiane" msgid "Low" msgstr "Faible" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Latence plus faible." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22004,7 +22077,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22102,7 +22175,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Afficher la caméra en miroir" @@ -22824,7 +22897,7 @@ msgid "My awesome sticker" msgstr "Mon super sticker" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Ma catégorie" @@ -24445,7 +24518,7 @@ msgstr "Continuez uniquement si vous avez commencé cette connexion sur un appar msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Seules les cartes de crédit sont acceptées. Les cartes de débit et prépayées ne peuvent pas servir à la vérification d’accès aux contenus pour adultes." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Haut-parleurs par défaut uniquement" @@ -24523,7 +24596,7 @@ msgstr "Seuls les membres et rôles sélectionnés pourront voir ce salon." msgid "Only show message previews for DMs with unread messages" msgstr "Afficher les aperçus de message uniquement pour les MP non lus" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Haut-parleurs seulement" @@ -24760,7 +24833,7 @@ msgstr "Ouvrir les détails du salon {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Ouvrir le chat" @@ -24841,7 +24914,7 @@ msgstr "Ouvrir l'emoji dans le navigateur" msgid "Open emoji picker" msgstr "Ouvrir le sélecteur d'emojis" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Ouvrir le flux d’événements" @@ -25281,6 +25354,16 @@ msgstr "Autres fils actifs" msgid "Other devices" msgstr "Autres appareils" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Les autres participants peuvent voir votre adresse IP." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Les autres participants d'un appel P2P peuvent voir votre adresse IP." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25396,6 +25479,22 @@ msgstr "Le transfert de propriété à <0>{username} n'a pas pu être effect msgid "Ownership transferred" msgstr "Propriété transférée" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Les appels P2P sont indisponibles pour le moment." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Appels P2P uniquement" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26097,7 +26196,7 @@ msgstr "Groupe épinglé" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Conversation de groupe épinglée" @@ -26537,7 +26636,7 @@ msgstr "Bouton d'aperçu" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Aperçu de la caméra" @@ -27108,7 +27207,7 @@ msgstr "Touche \" Appuyer pour parler \"" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Appuyer pour parler" @@ -31389,7 +31488,7 @@ msgid "Shorten the message and try again." msgstr "Raccourcissez le message et réessayez." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Raccourcissez le sujet et réessayez." @@ -31489,7 +31588,7 @@ msgstr "Afficher les contrôles de volume de connexion" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Afficher les raccourcis du menu contextuel" @@ -31574,7 +31673,7 @@ msgstr "Afficher moins d’effets sonores" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Afficher le contour de focus dans la zone de texte du chat" @@ -31702,7 +31801,7 @@ msgstr "Afficher les salons mis en sourdine" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Afficher ma propre caméra" @@ -31743,7 +31842,7 @@ msgstr "Afficher les nouveaux messages comme non lus" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Afficher les participants sans vidéo" @@ -32145,7 +32244,7 @@ msgstr "Appel silencieux" msgid "Silent calls" msgstr "Appels silencieux" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Silencieux lorsque l’appareil de capture n’a pas sa propre entrée audio" @@ -32656,8 +32755,8 @@ msgstr "Une erreur est survenue." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Une erreur est survenue. Veuillez réessayer dans un instant." @@ -32751,7 +32850,7 @@ msgstr "Sons" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Source" @@ -32860,7 +32959,7 @@ msgstr "Parlez maintenant. Appuyez sur Arrêter lorsque vous avez terminé. Vous #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Haut-parleur" @@ -33058,12 +33157,12 @@ msgid "Start a group DM" msgstr "Démarrer une conversation de groupe" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Démarrer un appel vidéo ?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Démarrer un appel vocal ?" @@ -33093,6 +33192,16 @@ msgstr "La date de début doit être antérieure à la date de fin." msgid "Start drag and drop" msgstr "Démarrer le glisser-déposer" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Démarrer en mode P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Démarrer en mode P2P ?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33118,6 +33227,11 @@ msgstr "Démarrer minimisé" msgid "Start over" msgstr "Recommencer" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Démarrer un appel P2P" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33132,6 +33246,11 @@ msgstr "Commencez vos recherches avec <0>{peoplePrefix} <1>{textChannelPrefi msgid "Start speaking" msgstr "Commencer la lecture" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Démarrer un appel standard" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33203,6 +33322,11 @@ msgstr "Panneau d’état" msgid "Status:" msgstr "Statut :" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Rester" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33410,7 +33534,7 @@ msgid "Stop" msgstr "Arrêter" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Arrêter d'être un canal d'annonces ?" @@ -33922,6 +34046,11 @@ msgstr "Passer à la communauté ou aux messages privés précédents" msgid "Switch to single column" msgstr "Passer en mode colonne unique" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Passer à l'appel standard" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37569,7 +37698,7 @@ msgstr "Groupe désépinglé" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "MP de groupe désépinglé" @@ -37728,6 +37857,11 @@ msgstr "E-mail non vérifié" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Jusqu'à {DISCOVERY_MAX_TAGS} tags aident les gens à trouver votre communauté. Ils apparaissent dans la recherche Découverte." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Jusqu'à {maxParticipants, plural, one {# personne} other {# personnes}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38042,7 +38176,7 @@ msgstr "Envoi des emojis" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39036,7 +39170,7 @@ msgstr "Envoi vidéo :" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Paramètres vidéo" @@ -39571,7 +39705,7 @@ msgstr "L'enregistrement vocal n'est pas pris en charge par ce navigateur." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Région vocale" @@ -39583,7 +39717,7 @@ msgstr "Barre de rappel de restauration de session vocale" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Paramètres vocaux" diff --git a/fluxer_app/src/features/i18n/locales/he/messages.po b/fluxer_app/src/features/i18n/locales/he/messages.po index b47dd6dd3..0003b9ddb 100644 --- a/fluxer_app/src/features/i18n/locales/he/messages.po +++ b/fluxer_app/src/features/i18n/locales/he/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# הודעה חסומה} other {# הודעות חסומות}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# ערוץ עוקב} other {# ערוצים עוקבים}} אחר הערוץ הזה. המרה שלו לערוץ טקסט תסיר את העוקבים האלה." @@ -2720,6 +2720,11 @@ msgstr "אימוג'י ברירת מחדל. אפשר להשתמש בו בכל מ msgid "A description is required." msgstr "נדרש תיאור." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "לא ניתן היה ליצור חיבור ישיר. אינכם יכולים לשמוע או לראות זה את זה." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "תצוגה מקדימה של קישור או הטמעה עשירה (לא msgid "A separate profile for each community" msgstr "פרופיל נפרד לכל קהילה" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "שיחת טלפון רגילה עוברת דרך שרת ועובדת עבור כולם." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "המנהלים השהו את ההזמנות, ולא ניתן להצטר #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "כל אחד יכול לבקש חשבון, אך מנהל מערכת חי msgid "Anyone in my communities" msgstr "כל אחד מהקהילות שלי" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "כל מי שלא יכול להתחבר ישירות לא יכול להשתתף עד שמישהו יעביר את השיחה לשיחה רגילה." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "כל מי שימצא אותך יוכל לשלוח בקשה. כבה זאת כדי לבחור קבוצות למטה." @@ -5112,6 +5127,16 @@ msgstr "שמע" msgid "Audio & video" msgstr "שמע ווידאו" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "אודיו ווידאו עוברים ישירות בין המשתתפים, עד {maxParticipants, plural, one {# אדם} other {# אנשים}}. כולם מסכימים לפני ההצטרפות ויכולים לראות את כתובת ה-IP של כולם." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "השמע והווידאו עוברים ישירות בין המשתתפים." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "הצעת השלמה אוטומטית: {label}, {selectedOptionPosition} #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "אוטומטי" @@ -6936,15 +6961,15 @@ msgstr "לא ניתן למחוק את החשבון" msgid "Capture" msgstr "לכידה" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "לכידת שמע מהאפליקציה" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "לכידת שמע מהמחשב" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "לכידת שמע מהמכשיר" @@ -7017,7 +7042,7 @@ msgstr "מזהה הקטגוריה הועתק" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "שם הקטגוריה" @@ -7554,7 +7579,7 @@ msgstr "מקטעי פרטי הערוץ" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "מזהה הערוץ הועתק" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "קישור לערוץ" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "הקישור לערוץ הועתק" @@ -7604,7 +7629,7 @@ msgstr "ניהול ערוצים" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "הערוץ סונכרן עם קטגוריית האב" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "נושא הערוץ ארוך מדי." @@ -7668,8 +7693,8 @@ msgstr "בחירת סוג ערוץ" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "הערוץ עודכן" @@ -9421,7 +9446,7 @@ msgstr "אישור לפני הצטרפות לערוצי קול" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "אישור לפני התחלת שיחות" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "שיחות" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "המר" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "הערוץ הומר לערוץ הכרזות" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "הפיכת ערוץ זה לערוץ טקסט תסיר כל ערוץ שעוקב אחריו." @@ -9949,7 +9974,7 @@ msgstr "העתקת מזהה ערוץ" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "העתקת קישור לערוץ" @@ -10379,13 +10404,18 @@ msgstr "ההתחברות לא הושלמה. יש לנסות שוב." msgid "Couldn't connect to {domain}. Try again." msgstr "לא הצלחנו להתחבר אל {domain}. נסה שוב." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "לא הצלחנו להתחבר אל {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "לא ניתן היה להתחבר לשיחה הקולית. יש לנסות שוב." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "לא ניתן היה להמיר את הערוץ הזה" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "לא ניתן היה להוריד את עדכון שולחן העבודה" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "לא ניתן לשכפל את הערוץ הזה" @@ -10511,6 +10541,11 @@ msgstr "לא ניתן היה להתעלם מבקשת החברות. יש לנסו msgid "Couldn't join community" msgstr "לא ניתן להצטרף לקהילה" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "לא ניתן היה להצטרף לשיחת ה-P2P. נסה שוב." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "לא ניתן לפתוח את פורטל החיוב" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "לא ניתן להצמיד את השיחה הזו" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "לא ניתן היה להריץ את הפקודה: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "לא ניתן היה לשמור את הגדרות הערוץ" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "לא ניתן היה לבטל את המעקב אחר ערוץ זה" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "לא ניתן לבטל את הצמדת השיחה הזו" @@ -11374,7 +11409,7 @@ msgstr "מותאם אישית" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "מותאם אישית" @@ -11938,7 +11973,7 @@ msgstr "ברירת מחדל" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "למחוק את \"{stickerName}\"? לא ניתן לבטל פעולה ז #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "למחוק את {channelLabel}? לא ניתן לבטל פעולה זו." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "שמע של שיתוף מסך במכשיר וידאו. {deviceLabel} הוא שם קלט השמע המשויך למכשיר הלכידה." #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "שמע מהמכשיר בלבד" @@ -12694,7 +12729,7 @@ msgstr "פרטי מכשיר" msgid "Device revoked" msgstr "המכשיר נותק" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "בחירת מכשיר" @@ -12758,6 +12793,11 @@ msgstr "גדלים שונים ומספר חברים שונה" msgid "Dim strikethrough text" msgstr "עמעום טקסט עם קו חוצה" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "חיבור ישיר. המדיה מוצפנת בין המשתתפים." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "סרגל צד של צ'אטים פרטיים" msgid "DNS TXT record" msgstr "רשומת DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "אל תשאל שוב, הסכם תמיד" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "שכפול" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "שכפול ערוץ" @@ -13825,7 +13870,7 @@ msgstr "עריכת קטגוריה" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "עריכת ערוץ" @@ -15004,7 +15049,7 @@ msgstr "מקש Escape" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "מקש Escape יוצא ממצב מקלדת" @@ -15047,6 +15092,11 @@ msgstr "כל קהילה שעוקבת אחרי הערוץ הזה תקבל עות msgid "Everyone" msgstr "כולם" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "כולם בשיחה מקבלים איכות סטרימינג של Plutonium בחינם: רזולוציית מקור של עד 4K ו-60 פריימים לשנייה." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "הוענקו {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "מעניק את כל ההרשאות ועוקף הגבלות ערוץ. רגיש במיוחד." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "בחירה פרטנית" @@ -17417,7 +17467,7 @@ msgstr "יוונית" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "תצוגת רשת" @@ -17909,7 +17959,7 @@ msgstr "הסתר משתתפים" msgid "Hide password" msgstr "הסתר סיסמה" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "הסתר תצוגה מקדימה ממוזערת" @@ -18322,11 +18372,11 @@ msgstr "התעלם" msgid "Ignore friend request" msgstr "התעלם מבקשת חברות" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "התעלם מהתקני חומרה" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "התעלם מקלט מדיה" @@ -18357,7 +18407,7 @@ msgstr "להתעלם מבקשת החברות מ-{displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "התעלם מהשהיית המצב האיטי של הערוץ הזה." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "התעלם מהתקנים וירטואליים" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "התקן קלט" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "רמת קלט" @@ -18865,7 +18915,7 @@ msgstr "ניטור קלט" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "פרופיל קלט" @@ -19431,6 +19481,12 @@ msgstr "ריצוד:" msgid "Join" msgstr "הצטרפות" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "הצטרף" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "הצטרפות לקהילה" msgid "Join a community to unlock stickers." msgstr "הצטרף לקהילה כדי לפתוח מדבקות." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "להצטרף לשיחת P2P?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "שיטת הצטרפות" msgid "Join on this device" msgstr "הצטרף במכשיר זה" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "הצטרף לשיחות P2P ללא אישור" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "לעזוב את {threadName}? זה פרטי, אז תוכל לחזור ר msgid "Leave blank for a random username" msgstr "השאר ריק כדי לקבל שם משתמש אקראי" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "יציאה מהשיחה" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "מאפשר ל-CSS מותאם אישית לחשוף אזורים שקופים בחלון לאחר הפעלה מחדש של אפליקציית המחשב." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "מאפשר לקהילות אחרות לעקוב אחר הערוץ הזה ולקבל עותקים של מה שאתה מפרסם." @@ -20729,6 +20797,11 @@ msgstr "לואיזיאנה" msgid "Low" msgstr "נמוכה" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "זמן תגובה נמוך יותר." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "מישיגן" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "מינסוטה" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "שיקוף מצלמה" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "המדבקה המדהימה שלי" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "הקטגוריה שלי" @@ -24444,7 +24517,7 @@ msgstr "המשך רק אם התחלת את הכניסה הזו במכשיר שב msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "מתקבלים כרטיסי אשראי בלבד. לא ניתן להשתמש בכרטיסי חיוב או בכרטיסים נטענים לצורך אימות התוכן למבוגרים." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "רמקולי ברירת המחדל בלבד" @@ -24522,7 +24595,7 @@ msgstr "רק חברים ותפקידים נבחרים יוכלו לראות את msgid "Only show message previews for DMs with unread messages" msgstr "הצג תצוגה מקדימה של הודעות רק בצ'אטים פרטיים עם הודעות שלא נקראו" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "רמקולים בלבד" @@ -24759,7 +24832,7 @@ msgstr "פתח פרטי ערוץ עבור {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "פתח צ'אט" @@ -24840,7 +24913,7 @@ msgstr "פתח אימוג'י בדפדפן" msgid "Open emoji picker" msgstr "פתח בורר אימוג'י" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "פתיחת יעד אירועים" @@ -25280,6 +25353,16 @@ msgstr "שרשורים פעילים אחרים" msgid "Other devices" msgstr "מכשירים אחרים" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "משתתפים אחרים יכולים לראות את כתובת ה-IP שלך." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "משתתפים אחרים בשיחת P2P יכולים לראות את כתובת ה-IP שלך." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "לא ניתן היה להעביר את הבעלות אל <0>{username}{peoplePrefix} <1>{textChannelPrefix msgid "Start speaking" msgstr "התחלת ההשמעה" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "התחל שיחה רגילה" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "לוח מצב" msgid "Status:" msgstr "סטטוס:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "הישאר" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "עצור" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "להפסיק להיות ערוץ הודעות?" @@ -33921,6 +34045,11 @@ msgstr "עבור לקהילה הקודמת או לצ'אטים פרטיים" msgid "Switch to single column" msgstr "החלף לעמודה בודדת" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "החלף לשיחה רגילה" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "קבוצה לא מוצמדת" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "צ'אט קבוצתי לא מוצמד" @@ -37727,6 +37856,11 @@ msgstr "אימייל לא מאומת" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "עד {DISCOVERY_MAX_TAGS} תגיות עוזרות לאנשים למצוא את הקהילה שלך. הן מופיעות בחיפוש בגילוי." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "עד {maxParticipants, plural, one {# אדם} other {# אנשים}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "מעלה אימוג'ים" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "שליחת וידאו:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "הגדרות וידאו" @@ -39570,7 +39704,7 @@ msgstr "הקלטת קול אינה נתמכת בדפדפן זה." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "אזור קולי" @@ -39582,7 +39716,7 @@ msgstr "שורת התראה לשחזור שיחה קולית" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "הגדרות קול" diff --git a/fluxer_app/src/features/i18n/locales/hi/messages.po b/fluxer_app/src/features/i18n/locales/hi/messages.po index bbf58d319..89e1e4131 100644 --- a/fluxer_app/src/features/i18n/locales/hi/messages.po +++ b/fluxer_app/src/features/i18n/locales/hi/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# ब्लॉक किया गया मैसेज} other {# ब्लॉक किए गए मैसेज}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# चैनल इसे फ़ॉलो करता है} other {# चैनल इसे फ़ॉलो करते हैं}}। इसे टेक्स्ट चैनल में बदलने पर वे फ़ॉलो हटा दिए जाएँगे।" @@ -2720,6 +2720,11 @@ msgstr "एक डिफ़ॉल्ट इमोजी। आप इसे {pro msgid "A description is required." msgstr "विवरण ज़रूरी है।" +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "सीधा कनेक्शन नहीं बन सका। आप एक-दूसरे को सुन या देख नहीं सकते।" + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "एक रिच एम्बेड या लिंक प्रीव msgid "A separate profile for each community" msgstr "हर समुदाय के लिए एक अलग प्रोफ़ाइल" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "एक सामान्य कॉल सर्वर से होकर जाती है और सभी के लिए काम करती है।" + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "एडमिन ने इनवाइट रोक दिए हैं #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "कोई भी अकाउंट के लिए अनुरोध msgid "Anyone in my communities" msgstr "मेरे समुदायों में कोई भी" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "जो कोई भी सीधे कनेक्ट नहीं कर सकता, वह तब तक भाग नहीं ले सकता जब तक कोई कॉल को स्टैंडर्ड कॉल में न बदल दे।" + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "जो कोई भी आपको ढूंढता है, वह अनुरोध भेज सकता है। नीचे दिए गए समूहों को चुनने के लिए इसे बंद कर दें।" @@ -5112,6 +5127,16 @@ msgstr "ऑडियो" msgid "Audio & video" msgstr "ऑडियो और वीडियो" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "ऑडियो और वीडियो सीधे प्रतिभागियों के बीच जाते हैं, {maxParticipants, plural, one {# व्यक्ति} other {# लोग}} तक। हर कोई जुड़ने से पहले सहमत होता है और एक-दूसरे का आईपी पता देख सकता है।" + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "ऑडियो और वीडियो सीधे प्रतिभागियों के बीच जाते हैं।" + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "ऑटो-कम्प्लीट सुझाव: {label}, {resultCoun #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "ऑटोमैटिक" @@ -6936,15 +6961,15 @@ msgstr "अकाउंट नहीं मिटाया जा सकता" msgid "Capture" msgstr "कैप्चर करें" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "ऐप ऑडियो कैप्चर करें" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "डेस्कटॉप ऑडियो कैप्चर करें" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "डिवाइस ऑडियो कैप्चर करें" @@ -7017,7 +7042,7 @@ msgstr "श्रेणी आईडी कॉपी की गई" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "श्रेणी का नाम" @@ -7554,7 +7579,7 @@ msgstr "चैनल विवरण सेक्शन" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "चैनल आईडी कॉपी की गई" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "चैनल लिंक" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "चैनल लिंक कॉपी किया गया" @@ -7604,7 +7629,7 @@ msgstr "चैनल प्रबंधन" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "चैनल पैरेंट श्रेणी से सिंक किया गया" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "चैनल का विषय बहुत लंबा है।" @@ -7668,8 +7693,8 @@ msgstr "चैनल का प्रकार चुनें" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "चैनल अपडेट किया गया" @@ -9421,7 +9446,7 @@ msgstr "वॉइस चैनल में शामिल होने से #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "कॉल शुरू करने से पहले पुष्टि करें" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "बातचीत" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "बदलें" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "चैनल को घोषणा चैनल में बदला गया" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "इस चैनल को टेक्स्ट चैनल में बदलने पर इससे जुड़े सभी चैनल हटा दिए जाएंगे।" @@ -9949,7 +9974,7 @@ msgstr "चैनल आईडी कॉपी करें" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "चैनल लिंक कॉपी करें" @@ -10379,13 +10404,18 @@ msgstr "साइन-इन पूरा नहीं हो सका। फि msgid "Couldn't connect to {domain}. Try again." msgstr "{domain} से कनेक्ट नहीं हो सका। फिर से प्रयास करें।" +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "{names} से कनेक्ट नहीं हो सका" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "वॉइस से कनेक्ट नहीं हो सका। कृपया फिर से कोशिश करें।" #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "इस चैनल को बदला नहीं जा सका" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "डेस्कटॉप अपडेट डाउनलोड नहीं किया जा सका" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "इस चैनल को डुप्लिकेट नहीं किया जा सका" @@ -10511,6 +10541,11 @@ msgstr "फ्रेंड रिक्वेस्ट को अनदेखा msgid "Couldn't join community" msgstr "कम्युनिटी में शामिल नहीं हो सके" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "P2P कॉल से जुड़ नहीं सके। पुनः प्रयास करें।" + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "बिलिंग पोर्टल नहीं खुल सका" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "इस बातचीत को पिन नहीं किया जा सका" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "वह कमांड नहीं चल सका: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "चैनल सेटिंग सेव नहीं हो सकी" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "इस चैनल को अनफ़ॉलो नहीं किया जा सका" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "इस बातचीत को अनपिन नहीं किया जा सका" @@ -11374,7 +11409,7 @@ msgstr "कस्टम" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "कस्टम" @@ -11938,7 +11973,7 @@ msgstr "डिफ़ॉल्ट" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "\"{stickerName}\" को मिटाएँ? इसे पूर्व #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "{channelLabel} को मिटाएँ? इसे पूर्ववत नहीं किया जा सकता।" @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "डिवाइस ऑडियो ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "डिवाइस ऑडियो ही" @@ -12694,7 +12729,7 @@ msgstr "डिवाइस विवरण" msgid "Device revoked" msgstr "डिवाइस हटा दिया गया" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "डिवाइस चयन" @@ -12758,6 +12793,11 @@ msgstr "अलग-अलग साइज़ और सदस्य संख् msgid "Dim strikethrough text" msgstr "स्ट्राइकथ्रू टेक्स्ट को हल्का करें" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "सीधा कनेक्शन। मीडिया प्रतिभागियों के बीच एन्क्रिप्टेड है।" + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "DM साइडबार" msgid "DNS TXT record" msgstr "DNS TXT रिकॉर्ड" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "फिर कभी न पूछें, हमेशा सहमत हों" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "डुप्लिकेट करें" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "चैनल डुप्लिकेट करें" @@ -13825,7 +13870,7 @@ msgstr "श्रेणी एडिट करें" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "चैनल एडिट करें" @@ -15004,7 +15049,7 @@ msgstr "एस्केप कुंजी" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "एस्केप कुंजी कीबोर्ड मोड से बाहर निकालती है" @@ -15047,6 +15092,11 @@ msgstr "इस चैनल को फॉलो करने वाले हर msgid "Everyone" msgstr "हर कोई" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "कॉल में हर कोई प्लूटोनियम स्ट्रीम क्वालिटी मुफ़्त में पाएगा: 4K तक सोर्स रिज़ॉल्यूशन और 60 fps।" + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "{permissions} प्रदान किए गए" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "सभी अनुमतियाँ देता है और चैनल प्रतिबंधों को बायपास करता है। अत्यधिक संवेदनशील।" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "बारीक चयन" @@ -17417,7 +17467,7 @@ msgstr "यूनानी" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "ग्रिड व्यू" @@ -17909,7 +17959,7 @@ msgstr "प्रतिभागी छिपाएँ" msgid "Hide password" msgstr "पासवर्ड छिपाएं" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "पूर्वावलोकन थंबनेल छिपाएँ" @@ -18322,11 +18372,11 @@ msgstr "अनदेखा करें" msgid "Ignore friend request" msgstr "फ्रेंड रिक्वेस्ट अनदेखा करें" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "हार्डवेयर डिवाइस को अनदेखा करें" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "इनपुट मीडिया को अनदेखा करें" @@ -18357,7 +18407,7 @@ msgstr "क्या आप {displayName} की फ्रेंड रिक् msgid "Ignore this channel's slowmode cooldown." msgstr "इस चैनल के स्लोमोड कूलडाउन को अनदेखा करें।" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "वर्चुअल को अनदेखा करें" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "इनपुट डिवाइस" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "इनपुट स्तर" @@ -18865,7 +18915,7 @@ msgstr "इनपुट मॉनिटरिंग" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "इनपुट प्रोफ़ाइल" @@ -19431,6 +19481,12 @@ msgstr "जिटर:" msgid "Join" msgstr "शामिल हों" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "शामिल हों" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "कम्युनिटी में शामिल हों" msgid "Join a community to unlock stickers." msgstr "स्टिकर अनलॉक करने के लिए किसी कम्युनिटी से जुड़ें।" +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "P2P कॉल में शामिल हों?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "शामिल होने का तरीका" msgid "Join on this device" msgstr "इसी डिवाइस पर शामिल हों" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "बिना पूछे P2P कॉल में शामिल हों" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "क्या आप {threadName} छोड़ना चाहते ह msgid "Leave blank for a random username" msgstr "रैंडम यूज़रनेम के लिए खाली छोड़ दें" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "कॉल छोड़ें" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "डेस्कटॉप ऐप को रीस्टार्ट करने के बाद कस्टम CSS पारदर्शी विंडो क्षेत्र दिखा सकता है।" #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "अन्य समुदायों को इस चैनल को फ़ॉलो करने और आपके द्वारा प्रकाशित की गई सामग्री की प्रतियां प्राप्त करने की अनुमति देता है।" @@ -20729,6 +20797,11 @@ msgstr "लुइज़ियाना" msgid "Low" msgstr "कम" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "कम लेटेंसी।" + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "मिशिगन" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "मिनेसोटा" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "कैमरा मिरर करें" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "मेरा शानदार स्टिकर" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "मेरी श्रेणी" @@ -24444,7 +24517,7 @@ msgstr "तभी जारी रखें यदि आपने यह सा msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "केवल क्रेडिट कार्ड स्वीकार किए जाते हैं। वयस्क सामग्री की जाँच के लिए डेबिट और प्रीपेड कार्ड इस्तेमाल नहीं किए जा सकते।" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "केवल डिफ़ॉल्ट स्पीकर" @@ -24522,7 +24595,7 @@ msgstr "केवल चयनित सदस्य और भूमिका msgid "Only show message previews for DMs with unread messages" msgstr "केवल बिना पढ़े मैसेज वाले DM के लिए मैसेज प्रीव्यू दिखाएं" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "केवल स्पीकर" @@ -24759,7 +24832,7 @@ msgstr "{channelName} के चैनल विवरण खोलें" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "चैट खोलें" @@ -24840,7 +24913,7 @@ msgstr "ब्राउज़र में इमोजी खोलें" msgid "Open emoji picker" msgstr "इमोजी पिकर खोलें" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "इवेंट सिंक खोलें" @@ -25280,6 +25353,16 @@ msgstr "अन्य सक्रिय थ्रेड" msgid "Other devices" msgstr "अन्य डिवाइस" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "दूसरे प्रतिभागी आपका आईपी पता देख सकते हैं।" + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "P2P कॉल में अन्य प्रतिभागी आपका IP पता देख सकते हैं।" + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "इस समय <0>{username} को स्वामित्व msgid "Ownership transferred" msgstr "स्वामित्व ट्रांसफ़र किया गया" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "अभी पीयर-टू-पीयर कॉल उपलब्ध नहीं हैं।" + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "सिर्फ पी2पी कॉल" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "पिन किया गया ग्रुप" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "पिन किया गया ग्रुप DM" @@ -26536,7 +26635,7 @@ msgstr "प्रीव्यू बटन" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "कैमरा प्रीव्यू देखें" @@ -27107,7 +27206,7 @@ msgstr "पुश टू टॉक कुंजी" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "पुश-टू-टॉक" @@ -31387,7 +31486,7 @@ msgid "Shorten the message and try again." msgstr "मैसेज छोटा करें और फिर से कोशिश करें।" #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "विषय छोटा करें और फिर से कोशिश करें।" @@ -31487,7 +31586,7 @@ msgstr "कनेक्शन वॉल्यूम कंट्रोल दि #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "कॉन्टेक्स्ट मेन्यू शॉर्टकट दिखाएं" @@ -31572,7 +31671,7 @@ msgstr "कम ध्वनि प्रभाव दिखाएं" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "चैट टेक्स्ट एरिया पर फ़ोकस रिंग दिखाएं" @@ -31700,7 +31799,7 @@ msgstr "म्यूट किए गए चैनल दिखाएं" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "मेरा कैमरा दिखाएं" @@ -31741,7 +31840,7 @@ msgstr "नए पोस्ट को अनरीड दिखाएं" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "बिना वीडियो वाले प्रतिभागी दिखाएं" @@ -32143,7 +32242,7 @@ msgstr "साइलेंट कॉल" msgid "Silent calls" msgstr "साइलेंट कॉल" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "जब कैप्चर डिवाइस में अपना कोई ऑडियो इनपुट न हो तो साइलेंट रहें" @@ -32654,8 +32753,8 @@ msgstr "कुछ गड़बड़ हो गई।" #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "कुछ गड़बड़ हो गई। कृपया कुछ देर बाद फिर से कोशिश करें।" @@ -32749,7 +32848,7 @@ msgstr "साउंड" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "सोर्स" @@ -32858,7 +32957,7 @@ msgstr "अभी बोलें। पूरा होने पर रोक #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "स्पीकर" @@ -33056,12 +33155,12 @@ msgid "Start a group DM" msgstr "ग्रुप DM शुरू करें" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "वीडियो कॉल शुरू करें?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "वॉइस कॉल शुरू करें?" @@ -33091,6 +33190,16 @@ msgstr "शुरुआती तारीख, आखिरी तारीख msgid "Start drag and drop" msgstr "खींचकर छोड़ना शुरू करें" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "P2P मोड में शुरू करें" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "P2P मोड में शुरू करें?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33116,6 +33225,11 @@ msgstr "मिनिमाइज़ करके शुरू करें" msgid "Start over" msgstr "फिर से शुरू करें" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "P2P कॉल शुरू करें" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33130,6 +33244,11 @@ msgstr "परिणाम सीमित करने के लिए खो msgid "Start speaking" msgstr "बोलना शुरू करें" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "मानक कॉल शुरू करें" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33201,6 +33320,11 @@ msgstr "स्टेटस स्लेट" msgid "Status:" msgstr "स्थिति:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "रहें" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33408,7 +33532,7 @@ msgid "Stop" msgstr "रोकें" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "क्या घोषणा चैनल बनना बंद करें?" @@ -33920,6 +34044,11 @@ msgstr "पिछली कम्युनिटी या DM पर स्व msgid "Switch to single column" msgstr "एक कॉलम पर स्विच करें" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "स्टैंडर्ड कॉल पर स्विच करें" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37567,7 +37696,7 @@ msgstr "अनपिन किया गया ग्रुप" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "ग्रुप DM अनपिन किया गया" @@ -37726,6 +37855,11 @@ msgstr "अपुष्ट ईमेल" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "ज़्यादा से ज़्यादा {DISCOVERY_MAX_TAGS} टैग लोगों को आपकी कम्युनिटी ढूंढने में मदद करते हैं। वे डिस्कवरी सर्च में दिखते हैं।" +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "अधिकतम {maxParticipants, plural, one {# व्यक्ति} other {# लोग}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38040,7 +38174,7 @@ msgstr "इमोजी अपलोड हो रहे हैं" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39034,7 +39168,7 @@ msgstr "वीडियो सेंड:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "वीडियो सेटिंग्स" @@ -39569,7 +39703,7 @@ msgstr "इस ब्राउज़र में वॉइस रिकॉर #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "वॉइस रीजन" @@ -39581,7 +39715,7 @@ msgstr "वॉइस सेशन रीस्टोर नागबार" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "वॉइस सेटिंग्स" diff --git a/fluxer_app/src/features/i18n/locales/hr/messages.po b/fluxer_app/src/features/i18n/locales/hr/messages.po index 5c0d88bd8..36cfb975f 100644 --- a/fluxer_app/src/features/i18n/locales/hr/messages.po +++ b/fluxer_app/src/features/i18n/locales/hr/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# blokirana poruka} few {# blokirane poruke} other {# blokiranih poruka}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# kanal ga prati} other {# kanala ga prati}} ovu poruku. Pretvaranje u tekstualni kanal uklonit će te pratitelje." @@ -2720,6 +2720,11 @@ msgstr "Zadana emoji. Možeš je koristiti bilo gdje na {productName}." msgid "A description is required." msgstr "Opis je obavezan." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Izravna veza nije uspostavljena. Ne možete se čuti ni vidjeti." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "bogati ugrađeni sadržaj ili pretpregled poveznice (nije prenesena dato msgid "A separate profile for each community" msgstr "Odvojeni profil za svaku zajednicu" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Standardni poziv prolazi kroz poslužitelj i radi za sve." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Administratori su pauzirali pozivnice — trenutačno se ne možete prid #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Svatko može zatražiti račun, ali administrator ga mora odobriti." msgid "Anyone in my communities" msgstr "Svatko u mojim zajednicama" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Svatko tko se ne može izravno povezati ne može sudjelovati dok netko ne prebaci poziv na standardni poziv." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Svatko tko te pronađe može poslati zahtjev. Isključi ovo da bi odabrao grupe ispod." @@ -5112,6 +5127,16 @@ msgstr "Zvuk" msgid "Audio & video" msgstr "Audio i video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Audio i video idu izravno između sudionika, do {maxParticipants, plural, one {# osoba} other {# osoba}}. Svi se slažu prije pridruživanja i mogu vidjeti IP adresu jedni drugima." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Audio i video idu izravno između sudionika." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Prijedlog automatskog dovršavanja: {label}, {selectedOptionPosition} od #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatski" @@ -6936,15 +6961,15 @@ msgstr "Nije moguće izbrisati račun" msgid "Capture" msgstr "Snimanje" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Snimanje zvuka aplikacije" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Snimanje zvuka s radne površine" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Snimanje zvuka uređaja" @@ -7017,7 +7042,7 @@ msgstr "ID kategorije kopiran" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Naziv kategorije" @@ -7554,7 +7579,7 @@ msgstr "Odjeljci s detaljima kanala" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID kanala kopiran" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "poveznica kanala" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Poveznica kanala kopirana" @@ -7604,7 +7629,7 @@ msgstr "Upravljanje kanalima" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Kanal sinkroniziran s nadređenom kategorijom" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Tema kanala je predugačka." @@ -7668,8 +7693,8 @@ msgstr "Odabir vrste kanala" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Kanal ažuriran" @@ -9421,7 +9446,7 @@ msgstr "Potvrdi prije pridruživanja glasovnim kanalima" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Potvrdi prije pokretanja poziva" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Razgovori" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Pretvori" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Pretvoreno u kanal za objave" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Pretvaranje ovog kanala u tekstualni kanal uklonit će sve kanale koji ga prate." @@ -9949,7 +9974,7 @@ msgstr "Kopiraj ID kanala" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Kopiraj poveznicu kanala" @@ -10379,13 +10404,18 @@ msgstr "Nije moguće dovršiti prijavu. Pokušajte ponovno." msgid "Couldn't connect to {domain}. Try again." msgstr "Nismo se mogli povezati na {domain}. Pokušajte ponovno." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Nismo se mogli povezati s {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Nije moguće povezati se s glasovnim kanalom. Pokušajte ponovno." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Nije moguće pretvoriti ovaj kanal" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Nije moguće preuzeti ažuriranje za stolno računalo" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Nije moguće duplicirati ovaj kanal" @@ -10511,6 +10541,11 @@ msgstr "Nije moguće zanemariti zahtjev za prijateljstvo. Pokušajte ponovno." msgid "Couldn't join community" msgstr "Nije moguće pridružiti se zajednici" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Nismo se mogli pridružiti P2P pozivu. Pokušajte ponovno." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Nije moguće otvoriti portal za naplatu" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Nije moguće prikvačiti ovaj razgovor" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Nije moguće pokrenuti tu naredbu: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Nije moguće spremiti postavke kanala" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Nije moguće otpratiti ovaj kanal" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Nije moguće otkvačiti ovaj razgovor" @@ -11374,7 +11409,7 @@ msgstr "Prilagođeno" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Prilagođeno" @@ -11938,7 +11973,7 @@ msgstr "Zadano" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Izbrisati \"{stickerName}\"? Ne može se poništiti." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Izbrisati {channelLabel}? Ne može se poništiti." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Zvuk uređaja ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Samo zvuk uređaja" @@ -12694,7 +12729,7 @@ msgstr "Detalji uređaja" msgid "Device revoked" msgstr "Uređaj opozvan" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Odabir uređaja" @@ -12758,6 +12793,11 @@ msgstr "Različite veličine i broj članova" msgid "Dim strikethrough text" msgstr "Priguši precrtani tekst" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Izravna veza. Mediji su šifrirani između sudionika." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Bočna traka za izravne poruke" msgid "DNS TXT record" msgstr "DNS TXT zapis" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Ne pitaj ponovno, uvijek se slaži" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Dupliciraj" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Dupliciraj kanal" @@ -13825,7 +13870,7 @@ msgstr "Uredi kategoriju" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Uredi kanal" @@ -15004,7 +15049,7 @@ msgstr "Tipka Escape" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Tipka Esc izlazi iz načina rada tipkovnice" @@ -15047,6 +15092,11 @@ msgstr "Svaka zajednica koja prati ovaj kanal dobit će kopiju. Ako je kasnije u msgid "Everyone" msgstr "Svi" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Svi u pozivu besplatno dobivaju kvalitetu Plutonium streama: do 4K izvorne rezolucije i 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Dodijeljena {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Daje sva dopuštenja i zaobilazi ograničenja kanala. Vrlo osjetljivo." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Detaljan odabir" @@ -17417,7 +17467,7 @@ msgstr "Grčki" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Prikaz rešetke" @@ -17909,7 +17959,7 @@ msgstr "Sakrij sudionike" msgid "Hide password" msgstr "Sakrij lozinku" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Sakrij sličicu pretpregleda" @@ -18322,11 +18372,11 @@ msgstr "Zanemari" msgid "Ignore friend request" msgstr "Zanemari zahtjev za prijateljstvo" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Zanemari hardverske uređaje" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Zanemari ulazne medije" @@ -18357,7 +18407,7 @@ msgstr "Zanemariti zahtjev za prijateljstvo od {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Zaobiđi ograničenje usporenog načina rada ovog kanala." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Zanemari virtualne uređaje" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Ulazni uređaj" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Ulazna razina" @@ -18865,7 +18915,7 @@ msgstr "Praćenje unosa" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Ulazni profil" @@ -19431,6 +19481,12 @@ msgstr "Podrhtavanje:" msgid "Join" msgstr "Pridruži se" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Pridruži se" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Pridružite se zajednici" msgid "Join a community to unlock stickers." msgstr "Pridružite se zajednici da biste otključali naljepnice." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Pridružiti P2P pozivu?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Način pridruživanja" msgid "Join on this device" msgstr "Pridruži se na ovom uređaju" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Pridruži se P2P pozivima bez pitanja" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Napustiti {threadName}? Privatan je, tako da se možeš vratiti samo ako msgid "Leave blank for a random username" msgstr "Ostavite prazno za nasumično korisničko ime" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Napusti poziv" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Omogućuje prilagođenom CSS-u da otkrije prozirna područja prozora nakon ponovnog pokretanja aplikacije za stolna računala." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Omogućuje drugim zajednicama da prate ovaj kanal i primaju kopije onoga što objavite." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Niska" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Niža latencija." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Zrcali kameru" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Moja super naljepnica" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Moja kategorija" @@ -24444,7 +24517,7 @@ msgstr "Nastavi samo ako si započeo/la ovu prijavu na uređaju koji kontrolira msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Prihvaćaju se samo kreditne kartice. Debitne i prepaid kartice ne mogu se koristiti za provjeru sadržaja za odrasle." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Samo zadani zvučnici" @@ -24522,7 +24595,7 @@ msgstr "Samo odabrani članovi i uloge moći će vidjeti ovaj kanal." msgid "Only show message previews for DMs with unread messages" msgstr "Prikazuj pretpreglede poruka samo za izravne poruke s nepročitanim porukama" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Samo zvučnici" @@ -24759,7 +24832,7 @@ msgstr "Otvori pojedinosti kanala {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Otvori čavrljanje" @@ -24840,7 +24913,7 @@ msgstr "Otvori emoji u pregledniku" msgid "Open emoji picker" msgstr "Otvori birač emojija" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Otvori sink događaja" @@ -25280,6 +25353,16 @@ msgstr "Ostale aktivne teme" msgid "Other devices" msgstr "Ostali uređaji" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Ostali sudionici mogu vidjeti vašu IP adresu." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Ostali sudionici P2P poziva mogu vidjeti vašu IP adresu." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Vlasništvo trenutačno nije moguće prenijeti na <0>{username}." msgid "Ownership transferred" msgstr "Vlasništvo preneseno" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "P2P pozivi trenutno nisu dostupni." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Samo P2P pozivi" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Prikvačena grupa" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Prikvačena grupna izravna poruka" @@ -26536,7 +26635,7 @@ msgstr "Gumb za pretpregled" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Pretpregled kamere" @@ -27107,7 +27206,7 @@ msgstr "Tipka za pritisni za govor" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Pritisni za govor" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Skratite poruku i pokušajte ponovno." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Skratite temu i pokušajte ponovno." @@ -31488,7 +31587,7 @@ msgstr "Prikaži kontrole glasnoće veze" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Prikaži prečace kontekstnog izbornika" @@ -31573,7 +31672,7 @@ msgstr "Prikaži manje zvučnih efekata" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Prikaži prsten fokusa na tekstualnom polju za chat" @@ -31701,7 +31800,7 @@ msgstr "Prikaži utišane kanale" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Prikaži moju kameru" @@ -31742,7 +31841,7 @@ msgstr "Prikaži nove objave kao nepročitane" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Prikaži sudionike bez videa" @@ -32144,7 +32243,7 @@ msgstr "Tihi poziv" msgid "Silent calls" msgstr "Tihi pozivi" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Tiho kada uređaj za snimanje nema vlastiti audio ulaz" @@ -32655,8 +32754,8 @@ msgstr "Nešto je pošlo po zlu." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Nešto je pošlo po zlu. Pokušajte ponovno za nekoliko trenutaka." @@ -32750,7 +32849,7 @@ msgstr "Zvukovi" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Izvorno" @@ -32859,7 +32958,7 @@ msgstr "Govorite sada. Pritisnite Zaustavi kada završite — snimku možete obr #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Zvučnik" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Započni grupnu izravnu poruku" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Započeti videopoziv?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Započeti glasovni poziv?" @@ -33092,6 +33191,16 @@ msgstr "Datum početka mora biti prije datuma završetka." msgid "Start drag and drop" msgstr "Pokreni povlačenje i ispuštanje" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Započni u P2P načinu" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Započeti u P2P načinu?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Pokreni umanjeno" msgid "Start over" msgstr "Počni ispočetka" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Pokreni P2P poziv" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Započnite pretraživanje s <0>{peoplePrefix} <1>{textChannelPrefix} msgid "Start speaking" msgstr "Započni čitanje" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Započni standardni poziv" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Statusna ploča" msgid "Status:" msgstr "Status:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Ostani" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Zaustavi" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Prekinuti objave u kanalu?" @@ -33921,6 +34045,11 @@ msgstr "Prebaci na prethodnu zajednicu ili izravne poruke" msgid "Switch to single column" msgstr "Prebaci na jedan stupac" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Prebaci na standardni poziv" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Grupa je otkvačena" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Grupna izravna poruka je otkvačena" @@ -37727,6 +37856,11 @@ msgstr "Nepotvrđena e-pošta" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Do {DISCOVERY_MAX_TAGS} oznaka pomaže ljudima da pronađu vašu zajednicu. Prikazuju se u pretraživanju Otkrivanja." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Do # {maxParticipants, plural, one {osobe} other {osoba}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Učitavanje emojija" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Slanje videa:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Postavke videa" @@ -39570,7 +39704,7 @@ msgstr "Snimanje glasa nije podržano u ovom pregledniku." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Glasovna regija" @@ -39582,7 +39716,7 @@ msgstr "Traka s obavijestima o vraćanju glasovne sesije" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Postavke glasa" diff --git a/fluxer_app/src/features/i18n/locales/hu/messages.po b/fluxer_app/src/features/i18n/locales/hu/messages.po index d11c4773e..d00ddf6b4 100644 --- a/fluxer_app/src/features/i18n/locales/hu/messages.po +++ b/fluxer_app/src/features/i18n/locales/hu/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# letiltott üzenet} other {# letiltott üzenet}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "Ez a csatorna követőinek száma: {count, plural, one {# csatorna követi} other {# csatorna követi}} ezt a csatornát. Szöveges csatornává alakítva ezek a követések megszűnnek." @@ -2720,6 +2720,11 @@ msgstr "Egy alapértelmezett emoji. Bárhol használhatod a(z) {productName} fel msgid "A description is required." msgstr "Leírás megadása kötelező." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Nem sikerült közvetlen kapcsolatot létesíteni. Nem halljátok és nem látjátok egymást." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "formázott beágyazott tartalom vagy linkelőnézet (nem feltöltött f msgid "A separate profile for each community" msgstr "Külön profil minden közösséghez" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Egy normál hívás szerveren keresztül megy, és mindenkinek működik." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Az adminok szüneteltetik a meghívókat – most nem tudsz csatlakozni. #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Bárki kérhet fiókot, de egy rendszergazdának jóvá kell hagynia." msgid "Anyone in my communities" msgstr "Bárki a közösségeimben" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Aki nem tud közvetlenül csatlakozni, az nem vehet részt, amíg valaki át nem kapcsolja a hívást egy standard hívásra." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Bárki, aki megtalál, kérhet. Kapcsold ki ezt, hogy kiválaszd az alábbi csoportokat." @@ -5112,6 +5127,16 @@ msgstr "Hang" msgid "Audio & video" msgstr "Hang és videó" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "A hang- és videóadatok közvetlenül a résztvevők között cserélnek gazdát, legfeljebb {maxParticipants, plural, one {# személy} other {# személy}} esetén. Mindenki beleegyezik a csatlakozás előtt, és látják egymás IP-címét." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "A hang- és videóadatok közvetlenül a résztvevők között haladnak." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Automatikus kiegészítési javaslat: {label}, {selectedOptionPosition}/ #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatikus" @@ -6936,15 +6961,15 @@ msgstr "A fiók nem törölhető" msgid "Capture" msgstr "Rögzítés" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Alkalmazáshang rögzítése" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Asztali hang rögzítése" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Eszköz hangjának rögzítése" @@ -7017,7 +7042,7 @@ msgstr "Kategóriaazonosító másolva" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Kategória neve" @@ -7554,7 +7579,7 @@ msgstr "Csatornarészletek szakaszai" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Csatornaazonosító kimásolva" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "csatornalink" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Csatorna linkje kimásolva" @@ -7604,7 +7629,7 @@ msgstr "Csatornakezelés" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "A csatorna szinkronizálva van a szülőkategóriával" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "A csatorna témája túl hosszú." @@ -7668,8 +7693,8 @@ msgstr "Csatornatípus kiválasztása" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "A csatorna frissítve" @@ -9421,7 +9446,7 @@ msgstr "Megerősítés hangcsatornákhoz való csatlakozás előtt" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Megerősítés hívás indítása előtt" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Beszélgetések" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Konvertálás" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Átalakítva bejelentési csatornává" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Ennek a csatornának szöveges csatornává alakítása eltávolítja az összes hozzá kapcsolódó csatornát." @@ -9949,7 +9974,7 @@ msgstr "Csatornaazonosító másolása" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Csatornalink másolása" @@ -10379,13 +10404,18 @@ msgstr "Nem sikerült befejezni a bejelentkezést. Próbáld újra." msgid "Couldn't connect to {domain}. Try again." msgstr "Nem sikerült csatlakozni a(z) {domain} szolgáltatáshoz. Próbáld újra." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Nem sikerült csatlakozni ide: {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Nem sikerült csatlakozni a hangcsatornához. Próbáld újra." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Nem sikerült átalakítani ezt a csatornát" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Nem sikerült letölteni az asztali frissítést" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Nem sikerült másolatot készíteni erről a csatornáról" @@ -10511,6 +10541,11 @@ msgstr "Nem sikerült figyelmen kívül hagyni az ismerősjelölést. Próbáld msgid "Couldn't join community" msgstr "Nem sikerült csatlakozni a közösséghez" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Nem sikerült csatlakozni a P2P híváshoz. Próbáld újra." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Nem sikerült megnyitni a számlázási portált" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Nem lehetett rögzíteni ezt a beszélgetést" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Nem sikerült futtatni a parancsot: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Nem sikerült menteni a csatorna beállításait" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Nem tudtuk eltávolítani a csatornát a követésből" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Nem lehetett feloldani a beszélgetés rögzítését" @@ -11374,7 +11409,7 @@ msgstr "Egyéni" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Egyéni" @@ -11938,7 +11973,7 @@ msgstr "Alapértelmezett" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Törlöd a(z) „{stickerName}” matricát? Ez a művelet nem vonható #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Törlöd a(z) {channelLabel} csatornát? Ez nem vonható vissza." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Eszköz hangja ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Csak az eszköz hangja" @@ -12694,7 +12729,7 @@ msgstr "Eszköz részletei" msgid "Device revoked" msgstr "Az eszköz hozzáférése visszavonva" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Eszközválasztás" @@ -12758,6 +12793,11 @@ msgstr "Különböző méretek és taglétszámok" msgid "Dim strikethrough text" msgstr "Áthúzott szöveg halványítása" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Közvetlen kapcsolat. A média titkosítva van a résztvevők között." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "DM-oldalsáv" msgid "DNS TXT record" msgstr "DNS TXT rekord" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Ne kérdezd meg újra, mindig egyezz bele" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Másolás" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Csatorna duplikálása" @@ -13825,7 +13870,7 @@ msgstr "Kategória szerkesztése" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Csatorna szerkesztése" @@ -15004,7 +15049,7 @@ msgstr "Esc billentyű" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Az Esc billentyűvel léphetsz ki a billentyűzetes módból" @@ -15047,6 +15092,11 @@ msgstr "Minden követő közösség kap egy másolatot. Ha később szerkeszted msgid "Everyone" msgstr "Mindenki" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Mindenki számára ingyenes a Plutonium stream minőség a hívásban: akár 4K felbontás és 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Megadva: {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Minden jogosultságot megad, és megkerüli a csatornakorlátozásokat. Különösen érzékeny jogosultság." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Részletes kiválasztás" @@ -17417,7 +17467,7 @@ msgstr "Görög" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Rácsos nézet" @@ -17909,7 +17959,7 @@ msgstr "Résztvevők elrejtése" msgid "Hide password" msgstr "Jelszó elrejtése" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Előnézeti bélyegkép elrejtése" @@ -18322,11 +18372,11 @@ msgstr "Elutasítás" msgid "Ignore friend request" msgstr "Ismerősjelölés mellőzése" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Hardvereszközök figyelmen kívül hagyása" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Bemeneti média mellőzése" @@ -18357,7 +18407,7 @@ msgstr "{displayName} ismerősjelölésének mellőzése?" msgid "Ignore this channel's slowmode cooldown." msgstr "A csatorna lassú módjához tartozó várakozási idő figyelmen kívül hagyása." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Virtuális eszközök figyelmen kívül hagyása" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Bemeneti eszköz" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Bemeneti szint" @@ -18865,7 +18915,7 @@ msgstr "Bemeneti figyelés" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Bemeneti profil" @@ -19431,6 +19481,12 @@ msgstr "Jitter:" msgid "Join" msgstr "Csatlakozás" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Csatlakozás" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Csatlakozás közösséghez" msgid "Join a community to unlock stickers." msgstr "Csatlakozz egy közösséghez a matricák feloldásához." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Csatlakozol egy P2P híváshoz?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Csatlakozási mód" msgid "Join on this device" msgstr "Csatlakozás ezen az eszközön" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "P2P hívások megerősítés nélkül" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Elhagyod a(z) {threadName} szálat? Ez egy privát szál, így csak akko msgid "Leave blank for a random username" msgstr "Hagyd üresen, ha véletlenszerű felhasználónevet szeretnél" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Hívás elhagyása" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Az egyéni CSS lehetővé teszi az átlátszó ablakrégiók megjelenítését az asztali alkalmazás újraindítása után." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Más közösségek is követhetik ezt a csatornát, és másolatot kaphatnak a közzétett tartalmakról." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Alacsony" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Alacsonyabb késleltetés." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Kamera tükrözése" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Szuper matricám" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Saját kategória" @@ -24444,7 +24517,7 @@ msgstr "Csak akkor folytasd, ha olyan eszközön kezdted ezt a bejelentkezést, msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Csak hitelkártyát fogadunk el. Betéti és előre feltöltött kártya nem használható a felnőtt tartalomhoz való hozzáférés ellenőrzéséhez." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Csak az alapértelmezett hangszórók" @@ -24522,7 +24595,7 @@ msgstr "Csak a kiválasztott tagok és szerepkörök láthatják ezt a csatorná msgid "Only show message previews for DMs with unread messages" msgstr "Üzenetelőnézetek megjelenítése csak az olvasatlan üzeneteket tartalmazó közvetlen beszélgetéseknél" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Csak hangszórók" @@ -24759,7 +24832,7 @@ msgstr "Csatornarészletek megnyitása – {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Csevegés megnyitása" @@ -24840,7 +24913,7 @@ msgstr "Emoji megnyitása böngészőben" msgid "Open emoji picker" msgstr "Emojiválasztó megnyitása" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Eseménygyűjtő megnyitása" @@ -25280,6 +25353,16 @@ msgstr "Más aktív szál" msgid "Other devices" msgstr "Egyéb eszközök" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Más résztvevők láthatják az IP-címedet." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "A P2P hívás többi résztvevője látja az IP-címedet." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "A tulajdonjogot jelenleg nem lehet átruházni <0>{username} részé msgid "Ownership transferred" msgstr "Tulajdonjog átruházva" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "A P2P hívások jelenleg nem elérhetők." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Csak P2P hívások" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Rögzített csoport" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Rögzített csoportos beszélgetés" @@ -26536,7 +26635,7 @@ msgstr "Előnézet gomb" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Kamera előnézete" @@ -27107,7 +27206,7 @@ msgstr "Adásgomb billentyűje" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Adásgomb" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Rövidítsd le az üzenetet, és próbáld újra." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Rövidítsd le a témát, és próbáld újra." @@ -31488,7 +31587,7 @@ msgstr "Kapcsolati hangerőszabályzók megjelenítése" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Billentyűparancsok megjelenítése a helyi menüben" @@ -31573,7 +31672,7 @@ msgstr "Kevesebb hangeffektus megjelenítése" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Fókuszgyűrű megjelenítése a csevegés szövegmezőjén" @@ -31701,7 +31800,7 @@ msgstr "Némított csatornák megjelenítése" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Saját kamera megjelenítése" @@ -31742,7 +31841,7 @@ msgstr "Új üzenetek jelzése olvasatlanként" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Nem videós résztvevők megjelenítése" @@ -32144,7 +32243,7 @@ msgstr "Csengetés nélküli hívás" msgid "Silent calls" msgstr "Csengetés nélküli hívások" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Némítva, ha a rögzítőeszköznek nincs saját hangbemenete" @@ -32655,8 +32754,8 @@ msgstr "Valami hiba történt." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Hiba történt. Próbáld újra egy kis idő múlva." @@ -32750,7 +32849,7 @@ msgstr "Hangok" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Forrás" @@ -32859,7 +32958,7 @@ msgstr "Beszélj most. Ha végeztél, nyomd meg a Leállítás gombot – a felv #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Hangszóró" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Csoportos beszélgetés indítása" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Videóhívás indítása?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Hanghívás indítása?" @@ -33092,6 +33191,16 @@ msgstr "A kezdő dátumnak korábbinak kell lennie, mint a befejező dátum." msgid "Start drag and drop" msgstr "Húzás és ejtés indítása" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Indítás P2P módban" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Indítás P2P módban?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Indítás kis méretben" msgid "Start over" msgstr "Újrakezdés" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Indíts P2P hívást" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Kezdd a kereséseket az alábbiak szerint: <0>{peoplePrefix}, <1>{te msgid "Start speaking" msgstr "Felolvasás indítása" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Indíts szokásos hívást" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Állapotpanel" msgid "Status:" msgstr "Állapot:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Marad" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Leállítás" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Hirdetési csatorna megszüntetése?" @@ -33921,6 +34045,11 @@ msgstr "Váltás az előző közösségre vagy DM-ekre" msgid "Switch to single column" msgstr "Váltás egyoszlopos nézetre" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Átváltás normál hívásra" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Csoport rögzítése feloldva" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Rögzítés feloldva (csoportos beszélgetés)" @@ -37727,6 +37856,11 @@ msgstr "Nem ellenőrzött e-mail-cím" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Akár {DISCOVERY_MAX_TAGS} címke is segíthet az embereknek megtalálni a közösségedet. Ezek megjelennek a Felfedezés keresésben." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Legfeljebb {maxParticipants, plural, one {# személy} other {# személy}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Emojik feltöltése" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Videó küldése:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Videóbeállítások" @@ -39570,7 +39704,7 @@ msgstr "A hangfelvétel nem támogatott ebben a böngészőben." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Hangrégió" @@ -39582,7 +39716,7 @@ msgstr "Hanghívás-munkamenet visszaállítási értesítő" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Hangbeállítások" diff --git a/fluxer_app/src/features/i18n/locales/id/messages.po b/fluxer_app/src/features/i18n/locales/id/messages.po index 3b6660071..f6422d46d 100644 --- a/fluxer_app/src/features/i18n/locales/id/messages.po +++ b/fluxer_app/src/features/i18n/locales/id/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# pesan diblokir} other {# pesan diblokir}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# kanal mengikuti} other {# kanal mengikuti}} kanal ini. Mengonversinya menjadi kanal teks akan menghapus pengikut tersebut." @@ -2720,6 +2720,11 @@ msgstr "Emoji bawaan. Anda dapat menggunakannya di mana saja di {productName}." msgid "A description is required." msgstr "Deskripsi wajib diisi." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Koneksi langsung tidak dapat dibuat. Anda tidak dapat mendengar atau melihat satu sama lain." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "sematan kaya atau pratinjau tautan (bukan file yang diunggah)" msgid "A separate profile for each community" msgstr "Profil terpisah untuk setiap komunitas" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Panggilan standar melewati server dan berfungsi untuk semua orang." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Admin menjeda undangan. Kamu tidak bisa bergabung sekarang." #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Siapa pun dapat meminta akun, tetapi administrator harus menyetujuinya." msgid "Anyone in my communities" msgstr "Siapa pun di komunitas saya" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Siapa pun yang tidak dapat terhubung langsung tidak dapat berpartisipasi sampai seseorang mengalihkan panggilan ke panggilan standar." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Siapa pun yang menemukan Anda dapat mengirim permintaan. Matikan ini untuk memilih grup di bawah." @@ -5112,6 +5127,16 @@ msgstr "Suara" msgid "Audio & video" msgstr "Audio & video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Audio dan video langsung terhubung antar peserta, hingga {maxParticipants, plural, one {# orang} other {# orang}}. Semua orang setuju sebelum bergabung dan dapat melihat alamat IP satu sama lain." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Audio dan video dikirim langsung antar peserta." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Saran pelengkapan otomatis: {label}, {selectedOptionPosition} dari {resu #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Otomatis" @@ -6936,15 +6961,15 @@ msgstr "Tidak bisa menghapus akun" msgid "Capture" msgstr "Rekam" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Rekam audio aplikasi" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Rekam audio desktop" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Rekam audio perangkat" @@ -7017,7 +7042,7 @@ msgstr "ID kategori disalin" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Nama kategori" @@ -7554,7 +7579,7 @@ msgstr "Bagian detail saluran" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID saluran disalin" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "tautan saluran" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Tautan saluran disalin" @@ -7604,7 +7629,7 @@ msgstr "Manajemen saluran" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Saluran disinkronkan dengan kategori induk" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Topik saluran terlalu panjang." @@ -7668,8 +7693,8 @@ msgstr "Pilih jenis saluran" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Saluran diperbarui" @@ -9421,7 +9446,7 @@ msgstr "Konfirmasi sebelum bergabung ke saluran suara" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Konfirmasi sebelum memulai panggilan" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Percakapan" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Ubah" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Mengubah saluran menjadi saluran pengumuman" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Mengubah saluran ini menjadi saluran teks akan menghapus setiap saluran yang mengikutinya." @@ -9949,7 +9974,7 @@ msgstr "Salin ID saluran" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Salin tautan saluran" @@ -10379,13 +10404,18 @@ msgstr "Tidak dapat menyelesaikan proses masuk. Coba lagi." msgid "Couldn't connect to {domain}. Try again." msgstr "Tidak dapat terhubung ke {domain}. Coba lagi." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Tidak dapat terhubung ke {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Tidak dapat terhubung ke suara. Coba lagi." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Tidak dapat mengonversi saluran ini" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Tidak dapat mengunduh pembaruan desktop" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Tidak dapat menduplikasi saluran ini" @@ -10511,6 +10541,11 @@ msgstr "Tidak dapat mengabaikan permintaan pertemanan. Coba lagi." msgid "Couldn't join community" msgstr "Tidak dapat bergabung ke komunitas" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Tidak dapat bergabung ke panggilan P2P. Coba lagi." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Tidak dapat membuka portal penagihan" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Tidak dapat menyematkan percakapan ini" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Tidak dapat menjalankan perintah tersebut: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Tidak dapat menyimpan pengaturan saluran" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Tidak dapat berhenti mengikuti saluran ini" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Tidak dapat melepas sematan percakapan ini" @@ -11374,7 +11409,7 @@ msgstr "Kustom" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Kustom" @@ -11938,7 +11973,7 @@ msgstr "Default" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Hapus \"{stickerName}\"? Tidak dapat dibatalkan." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Hapus {channelLabel}? Tidak dapat dibatalkan." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Audio perangkat ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Audio perangkat saja" @@ -12694,7 +12729,7 @@ msgstr "Detail perangkat" msgid "Device revoked" msgstr "Akses perangkat dicabut" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Pilihan perangkat" @@ -12758,6 +12793,11 @@ msgstr "Ukuran & jumlah anggota berbeda" msgid "Dim strikethrough text" msgstr "Redupkan teks coret" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Koneksi langsung. Media terenkripsi antar peserta." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Bilah sisi DM" msgid "DNS TXT record" msgstr "Catatan DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Jangan tanya lagi, selalu setuju" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplikat" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplikat saluran" @@ -13825,7 +13870,7 @@ msgstr "Edit kategori" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Edit saluran" @@ -15004,7 +15049,7 @@ msgstr "Tombol Escape" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Tombol Escape keluar dari mode keyboard" @@ -15047,6 +15092,11 @@ msgstr "Setiap komunitas yang mengikuti saluran ini akan mendapatkan salinannya. msgid "Everyone" msgstr "Semua orang" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Semua orang dalam panggilan mendapatkan kualitas streaming Plutonium secara gratis: resolusi sumber hingga 4K dan 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Diberikan {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Memberikan semua izin dan melewati pembatasan saluran. Sangat sensitif." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Pilihan terperinci" @@ -17417,7 +17467,7 @@ msgstr "Yunani" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Tampilan petak" @@ -17909,7 +17959,7 @@ msgstr "Sembunyikan peserta" msgid "Hide password" msgstr "Sembunyikan kata sandi" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Sembunyikan gambar mini pratinjau" @@ -18322,11 +18372,11 @@ msgstr "Abaikan" msgid "Ignore friend request" msgstr "Abaikan permintaan pertemanan" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Abaikan perangkat keras" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Abaikan media masukan" @@ -18357,7 +18407,7 @@ msgstr "Abaikan permintaan pertemanan dari {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Abaikan jeda mode lambat saluran ini." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Abaikan virtual" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Perangkat input" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Tingkat input" @@ -18865,7 +18915,7 @@ msgstr "Pemantauan Input" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Profil input" @@ -19431,6 +19481,12 @@ msgstr "Jitter:" msgid "Join" msgstr "Gabung" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Gabung" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Gabung komunitas" msgid "Join a community to unlock stickers." msgstr "Gabung ke komunitas untuk membuka stiker." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Gabung ke panggilan P2P?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Metode gabung" msgid "Join on this device" msgstr "Gabung di perangkat ini" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Gabung panggilan P2P tanpa bertanya" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Tinggalkan {threadName}? Ini bersifat pribadi, jadi Anda hanya bisa kemb msgid "Leave blank for a random username" msgstr "Biarkan kosong untuk nama pengguna acak" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Tinggalkan panggilan" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Izinkan CSS kustom menampilkan area jendela transparan setelah memulai ulang aplikasi desktop." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Izinkan komunitas lain mengikuti saluran ini dan mendapatkan salinan dari apa yang Anda publikasikan." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Rendah" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Latensi lebih rendah." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Cerminkan kamera" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Stiker saya yang keren" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Kategori saya" @@ -24444,7 +24517,7 @@ msgstr "Lanjutkan hanya jika Anda memulai login ini di perangkat yang Anda kenda msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Hanya kartu kredit yang diterima. Kartu debit dan prabayar tidak dapat digunakan untuk pemeriksaan konten dewasa." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Hanya speaker default" @@ -24522,7 +24595,7 @@ msgstr "Hanya anggota dan peran yang dipilih yang dapat melihat saluran ini." msgid "Only show message previews for DMs with unread messages" msgstr "Hanya tampilkan pratinjau pesan untuk DM dengan pesan yang belum dibaca" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Hanya speaker" @@ -24759,7 +24832,7 @@ msgstr "Buka detail saluran untuk {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Buka obrolan" @@ -24840,7 +24913,7 @@ msgstr "Buka emoji di browser" msgid "Open emoji picker" msgstr "Buka pemilih emoji" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Buka event sink" @@ -25280,6 +25353,16 @@ msgstr "Thread aktif lainnya" msgid "Other devices" msgstr "Perangkat lain" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Peserta lain dapat melihat alamat IP Anda." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Peserta lain dalam panggilan P2P dapat melihat alamat IP Anda." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Kepemilikan tidak dapat dialihkan ke <0>{username} saat ini." msgid "Ownership transferred" msgstr "Kepemilikan dialihkan" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Panggilan P2P tidak tersedia saat ini." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Panggilan P2P saja" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Grup tersemat" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "DM grup tersemat" @@ -26536,7 +26635,7 @@ msgstr "Tombol pratinjau" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Pratinjau kamera" @@ -27107,7 +27206,7 @@ msgstr "Tombol push to talk" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Push-to-talk" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Persingkat pesan dan coba lagi." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Persingkat topik dan coba lagi." @@ -31488,7 +31587,7 @@ msgstr "Tampilkan kontrol volume koneksi" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Tampilkan pintasan menu konteks" @@ -31573,7 +31672,7 @@ msgstr "Tampilkan lebih sedikit efek suara" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Tampilkan cincin fokus di area teks obrolan" @@ -31701,7 +31800,7 @@ msgstr "Tampilkan saluran yang dibisukan" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Tampilkan kamera saya" @@ -31742,7 +31841,7 @@ msgstr "Tampilkan postingan baru sebagai belum dibaca" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Tampilkan peserta non-video" @@ -32144,7 +32243,7 @@ msgstr "Panggilan senyap" msgid "Silent calls" msgstr "Panggilan senyap" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Senyap saat perangkat penangkap tidak memiliki input audio sendiri" @@ -32655,8 +32754,8 @@ msgstr "Terjadi kesalahan." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Terjadi kesalahan. Silakan coba lagi sebentar lagi." @@ -32750,7 +32849,7 @@ msgstr "Suara" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Sumber" @@ -32859,7 +32958,7 @@ msgstr "Bicaralah sekarang. Tekan Berhenti setelah selesai — kamu bisa memangk #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Speaker" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Mulai DM grup" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Mulai panggilan video?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Mulai panggilan suara?" @@ -33092,6 +33191,16 @@ msgstr "Tanggal mulai harus sebelum tanggal berakhir." msgid "Start drag and drop" msgstr "Mulai seret dan lepas" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Mulai dalam mode P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Mulai dalam mode P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Mulai diminimalkan" msgid "Start over" msgstr "Mulai lagi" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Mulai panggilan P2P" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Mulai pencarian dengan <0>{peoplePrefix} <1>{textChannelPrefix} msgid "Start speaking" msgstr "Mulai bicara" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Mulai panggilan standar" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Panel status" msgid "Status:" msgstr "Status:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Tetap" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Berhenti" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Berhenti menjadi saluran pengumuman?" @@ -33921,6 +34045,11 @@ msgstr "Beralih ke komunitas atau DM sebelumnya" msgid "Switch to single column" msgstr "Beralih ke satu kolom" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Beralih ke panggilan standar" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Sematan grup dilepas" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Sematan DM grup dilepas" @@ -37727,6 +37856,11 @@ msgstr "Email belum terverifikasi" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Hingga {DISCOVERY_MAX_TAGS} tag membantu orang menemukan komunitasmu. Tag ini akan muncul di pencarian Discovery." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Hingga {maxParticipants, plural, one {# orang} other {# orang}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Mengunggah emoji" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Pengiriman video:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Pengaturan video" @@ -39570,7 +39704,7 @@ msgstr "Perekaman suara tidak didukung di browser ini." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Wilayah suara" @@ -39582,7 +39716,7 @@ msgstr "Bilah notifikasi pemulihan sesi suara" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Pengaturan suara" diff --git a/fluxer_app/src/features/i18n/locales/it/messages.po b/fluxer_app/src/features/i18n/locales/it/messages.po index 1a08b8b5b..5222e8bb1 100644 --- a/fluxer_app/src/features/i18n/locales/it/messages.po +++ b/fluxer_app/src/features/i18n/locales/it/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# messaggio bloccato} other {# messaggi bloccati}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# canale segue} other {# canali seguono}} questo canale. La conversione in un canale di testo rimuoverà quei seguiti." @@ -2720,6 +2720,11 @@ msgstr "Una emoji predefinita. Puoi usarla ovunque su {productName}." msgid "A description is required." msgstr "È richiesta una descrizione." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Non è stato possibile stabilire una connessione diretta. Non potete sentirvi né vedervi." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "un contenuto incorporato avanzato o un’anteprima del link (non un file msgid "A separate profile for each community" msgstr "Un profilo separato per ogni community" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Una chiamata standard passa attraverso un server e funziona per tutti." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Gli amministratori hanno messo in pausa gli inviti, non puoi unirti in q #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Chiunque può richiedere un account, ma un amministratore deve approvarl msgid "Anyone in my communities" msgstr "Chiunque nelle mie community" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Chiunque non riesca a connettersi direttamente non potrà partecipare finché qualcuno non passerà la chiamata a una chiamata standard." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Chiunque ti trovi può inviare una richiesta. Disattiva questa opzione per scegliere i gruppi qui sotto." @@ -5112,6 +5127,16 @@ msgstr "Audio" msgid "Audio & video" msgstr "Audio e video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Audio e video passano direttamente tra i partecipanti, fino a {maxParticipants, plural, one {# persona} other {# persone}}. Tutti concordano prima di unirsi e possono vedere l'indirizzo IP di ciascuno." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "L'audio e il video passano direttamente tra i partecipanti." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Suggerimento di completamento automatico: {label}, {selectedOptionPositi #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatico" @@ -6936,15 +6961,15 @@ msgstr "Impossibile eliminare l'account" msgid "Capture" msgstr "Acquisisci" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Acquisisci audio app" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Acquisisci audio desktop" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Acquisisci audio del dispositivo" @@ -7017,7 +7042,7 @@ msgstr "ID categoria copiato" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Nome categoria" @@ -7554,7 +7579,7 @@ msgstr "Sezioni dettagli canale" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID canale copiato" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "link al canale" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Link del canale copiato" @@ -7604,7 +7629,7 @@ msgstr "Gestione canali" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Canale sincronizzato con la categoria principale" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "L'argomento del canale è troppo lungo." @@ -7668,8 +7693,8 @@ msgstr "Selezione del tipo di canale" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Canale aggiornato" @@ -9421,7 +9446,7 @@ msgstr "Chiedi conferma prima di unirti ai canali vocali" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Chiedi conferma prima di avviare chiamate" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Conversazioni" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Converti" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Convertito il canale in un canale di annunci" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "La conversione di questo canale in un canale di testo rimuoverà tutti i canali che lo seguono." @@ -9949,7 +9974,7 @@ msgstr "Copia ID canale" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Copia link canale" @@ -10379,13 +10404,18 @@ msgstr "Accesso non completato. Riprova." msgid "Couldn't connect to {domain}. Try again." msgstr "Impossibile connettersi a {domain}. Riprova." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Impossibile connettersi a {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Impossibile connettersi alla chat vocale. Riprova." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Impossibile convertire questo canale" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Impossibile scaricare l'aggiornamento desktop" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Impossibile duplicare questo canale" @@ -10511,6 +10541,11 @@ msgstr "Impossibile ignorare la richiesta di amicizia. Riprova." msgid "Couldn't join community" msgstr "Impossibile unirsi alla community" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Impossibile partecipare alla chiamata P2P. Riprova." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Impossibile aprire il portale di fatturazione" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Impossibile fissare questa conversazione" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Impossibile eseguire il comando: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Impossibile salvare le impostazioni del canale" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Impossibile annullare l'iscrizione a questo canale" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Impossibile rimuovere questa conversazione da quelle fissate" @@ -11374,7 +11409,7 @@ msgstr "Personalizzato" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Personalizzato" @@ -11938,7 +11973,7 @@ msgstr "Predefinito" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Eliminare \"{stickerName}\"? Non sarà possibile annullare l'operazione. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Eliminare {channelLabel}? Non sarà possibile annullare l'operazione." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Audio del dispositivo ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Solo audio del dispositivo" @@ -12694,7 +12729,7 @@ msgstr "Dettagli del dispositivo" msgid "Device revoked" msgstr "Accesso del dispositivo revocato" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Selezione dispositivo" @@ -12758,6 +12793,11 @@ msgstr "Dimensioni e numero di membri diversi" msgid "Dim strikethrough text" msgstr "Attenua il testo barrato" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Connessione diretta. I media sono crittografati tra i partecipanti." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Barra laterale DM" msgid "DNS TXT record" msgstr "Record DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Non chiedere più, accetta sempre" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplica" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplica canale" @@ -13825,7 +13870,7 @@ msgstr "Modifica categoria" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Modifica canale" @@ -15004,7 +15049,7 @@ msgstr "Tasto Esc" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Il tasto Esc esce dalla modalità tastiera" @@ -15047,6 +15092,11 @@ msgstr "Ogni community che segue questo canale riceverà una copia. Se la modifi msgid "Everyone" msgstr "Tutti" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Tutti nella chiamata ottengono la qualità dello streaming Plutonium gratuitamente: risoluzione sorgente fino a 4K e 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Concesso {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Concede tutti i permessi e ignora le restrizioni del canale. Molto sensibile." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Selezione dettagliata" @@ -17417,7 +17467,7 @@ msgstr "Greco" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Visualizzazione a griglia" @@ -17909,7 +17959,7 @@ msgstr "Nascondi partecipanti" msgid "Hide password" msgstr "Nascondi password" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Nascondi miniatura di anteprima" @@ -18322,11 +18372,11 @@ msgstr "Ignora" msgid "Ignore friend request" msgstr "Ignora richiesta di amicizia" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignora dispositivi hardware" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignora i media in entrata" @@ -18357,7 +18407,7 @@ msgstr "Ignorare la richiesta di amicizia da {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignora l’intervallo di attesa della modalità lenta di questo canale." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignora virtuale" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Dispositivo di input" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Livello di input" @@ -18865,7 +18915,7 @@ msgstr "Monitoraggio input" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Profilo di input" @@ -19431,6 +19481,12 @@ msgstr "Jitter:" msgid "Join" msgstr "Unisciti" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Partecipa" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Unisciti a una community" msgid "Join a community to unlock stickers." msgstr "Unisciti a una community per sbloccare gli sticker." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Partecipare a una chiamata P2P?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Metodo di accesso" msgid "Join on this device" msgstr "Partecipa da questo dispositivo" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Partecipa alle chiamate P2P senza chiedere" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Lasciare {threadName}? È privato, quindi potrai rientrare solo se qualc msgid "Leave blank for a random username" msgstr "Lascia vuoto per un nome utente casuale" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Lascia la chiamata" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Consente al CSS personalizzato di rivelare le aree trasparenti della finestra dopo aver riavviato l'app desktop." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Permette ad altre community di seguire questo canale e ricevere copie di ciò che pubblichi." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Basso" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Latenza inferiore." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Rifletti fotocamera" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Il mio sticker fantastico" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "La mia categoria" @@ -24444,7 +24517,7 @@ msgstr "Continua solo se hai avviato questa accesso su un dispositivo che contro msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Sono accettate solo carte di credito. Le carte di debito e prepagate non possono essere utilizzate per la verifica dei contenuti per adulti." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Solo altoparlanti predefiniti" @@ -24522,7 +24595,7 @@ msgstr "Solo i membri e i ruoli selezionati potranno visualizzare questo canale. msgid "Only show message previews for DMs with unread messages" msgstr "Mostra anteprime messaggi solo per i DM con messaggi non letti" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Solo altoparlanti" @@ -24759,7 +24832,7 @@ msgstr "Apri i dettagli del canale {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Apri chat" @@ -24840,7 +24913,7 @@ msgstr "Apri emoji nel browser" msgid "Open emoji picker" msgstr "Apri selettore emoji" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Apri sink eventi" @@ -25280,6 +25353,16 @@ msgstr "Altre discussioni attive" msgid "Other devices" msgstr "Altri dispositivi" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Gli altri partecipanti possono vedere il tuo indirizzo IP." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Gli altri partecipanti a una chiamata P2P possono vedere il tuo indirizzo IP." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Non è stato possibile trasferire la proprietà a <0>{username} in q msgid "Ownership transferred" msgstr "Proprietà trasferita" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Le chiamate peer-to-peer non sono disponibili al momento." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Chiamate solo P2P" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Gruppo fissato" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "DM di gruppo fissato" @@ -26536,7 +26635,7 @@ msgstr "Pulsante di anteprima" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Anteprima fotocamera" @@ -27107,7 +27206,7 @@ msgstr "Tasto \"Premi per parlare\"" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Push-to-talk" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Accorcia il messaggio e riprova." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Accorcia l'argomento e riprova." @@ -31488,7 +31587,7 @@ msgstr "Mostra i controlli del volume di connessione" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Mostra scorciatoie menu contestuale" @@ -31573,7 +31672,7 @@ msgstr "Mostra meno effetti sonori" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Mostra il contorno di messa a fuoco nel campo di testo della chat" @@ -31701,7 +31800,7 @@ msgstr "Mostra canali silenziati" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Mostra la mia fotocamera" @@ -31742,7 +31841,7 @@ msgstr "Mostra i nuovi post come non letti" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Mostra partecipanti senza video" @@ -32144,7 +32243,7 @@ msgstr "Chiamata silenziosa" msgid "Silent calls" msgstr "Chiamate silenziose" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Silenzioso quando il dispositivo di acquisizione non ha un proprio input audio" @@ -32655,8 +32754,8 @@ msgstr "Qualcosa è andato storto." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Qualcosa è andato storto. Riprova tra un momento." @@ -32750,7 +32849,7 @@ msgstr "Suoni" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Sorgente" @@ -32859,7 +32958,7 @@ msgstr "Parla ora. Premi Ferma quando hai finito: potrai tagliare la registrazio #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Altoparlante" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Avvia un DM di gruppo" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Avviare una videochiamata?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Avviare una chiamata vocale?" @@ -33092,6 +33191,16 @@ msgstr "La data di inizio deve essere precedente alla data di fine." msgid "Start drag and drop" msgstr "Avvia trascinamento" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Avvia in modalità P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Avvia in modalità P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Avvia ridotto a icona" msgid "Start over" msgstr "Ricomincia" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Avvia chiamata P2P" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Inizia le ricerche con <0>{peoplePrefix} <1>{textChannelPrefix} msgid "Start speaking" msgstr "Avvia la riproduzione" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Avvia chiamata standard" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Riquadro di stato" msgid "Status:" msgstr "Stato:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Rimani" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Ferma" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Smettere di essere un canale di annunci?" @@ -33921,6 +34045,11 @@ msgstr "Passa alla community precedente o ai DM" msgid "Switch to single column" msgstr "Passa a colonna singola" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Passa alla chiamata standard" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Gruppo non fissato" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "DM di gruppo non fissato" @@ -37727,6 +37856,11 @@ msgstr "Email non verificata" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Fino a {DISCOVERY_MAX_TAGS} tag aiutano le persone a trovare la tua community. Vengono visualizzati nella ricerca di Scopri." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Fino a {maxParticipants, plural, one {# persona} other {# persone}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Caricamento emoji" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Invio video:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Impostazioni video" @@ -39570,7 +39704,7 @@ msgstr "La registrazione vocale non è supportata in questo browser." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Regione vocale" @@ -39582,7 +39716,7 @@ msgstr "Banner di ripristino della sessione vocale" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Impostazioni voce" diff --git a/fluxer_app/src/features/i18n/locales/ja/messages.po b/fluxer_app/src/features/i18n/locales/ja/messages.po index ec78255d8..9c007c70a 100644 --- a/fluxer_app/src/features/i18n/locales/ja/messages.po +++ b/fluxer_app/src/features/i18n/locales/ja/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {#件のブロックされたメッセージ} other {#件のブロックされたメッセージ}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# チャンネルがフォローしています} other {# チャンネルがフォローしています}}このチャンネルを。テキストチャンネルに変換すると、それらのフォローは削除されます。" @@ -2720,6 +2720,11 @@ msgstr "{productName} のどこでも使えるデフォルトの絵文字です msgid "A description is required." msgstr "説明は必須です。" +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "直接接続ができませんでした。お互いの声や映像を確認できません。" + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "リッチな埋め込みまたはリンクプレビュー(アップロ msgid "A separate profile for each community" msgstr "コミュニティごとのプロフィール" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "通常の通話はサーバーを経由するため、どなたでもご利用いただけます。" + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "管理者が招待を一時停止しました。現在参加すること #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "誰でもアカウントをリクエストできますが、管理者の msgid "Anyone in my communities" msgstr "コミュニティのメンバー全員" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "直接接続できない人も、誰かが標準通話に切り替えるまで参加できません。" + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "見つけた人は誰でもリクエストを送信できます。これをオフにすると、以下のグループを選択できます。" @@ -5112,6 +5127,16 @@ msgstr "オーディオ" msgid "Audio & video" msgstr "オーディオとビデオ" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "オーディオとビデオは参加者間で直接やり取りされます。最大 {maxParticipants, plural, one {# 人} other {# 人}} まで参加できます。参加前に全員が同意し、お互いのIPアドレスを確認できます。" + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "音声とビデオは参加者間で直接やり取りされます。" + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "入力候補: {label}、{resultCount}件中{selectedOptionPosition}番 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "自動" @@ -6936,15 +6961,15 @@ msgstr "アカウントを削除できません" msgid "Capture" msgstr "キャプチャ" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "アプリの音声をキャプチャ" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "デスクトップ音声をキャプチャ" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "デバイスの音声をキャプチャ" @@ -7017,7 +7042,7 @@ msgstr "カテゴリIDをコピーしました" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "カテゴリ名" @@ -7554,7 +7579,7 @@ msgstr "チャンネル詳細のセクション" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "チャンネルIDをコピーしました" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "チャンネルリンク" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "チャンネルリンクをコピーしました" @@ -7604,7 +7629,7 @@ msgstr "チャンネル管理" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "チャンネルを親カテゴリと同期しました" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "チャンネルのトピックが長すぎます。" @@ -7668,8 +7693,8 @@ msgstr "チャンネルの種類を選択" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "チャンネルを更新しました" @@ -9421,7 +9446,7 @@ msgstr "ボイスチャンネル参加時に確認する" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "通話開始前に確認する" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "会話" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "変換" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "チャンネルをアナウンスチャンネルに変換しました" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "このチャンネルをテキストチャンネルに変換すると、フォローしているすべてのチャンネルが削除されます。" @@ -9949,7 +9974,7 @@ msgstr "チャンネルIDをコピー" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "チャンネルリンクをコピー" @@ -10379,13 +10404,18 @@ msgstr "サインインを完了できませんでした。もう一度お試し msgid "Couldn't connect to {domain}. Try again." msgstr "{domain} に接続できませんでした。もう一度お試しください。" +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "{names}に接続できませんでした" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "ボイスに接続できませんでした。もう一度お試しください。" #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "このチャンネルを変換できませんでした" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "デスクトップ版のアップデートをダウンロードできませんでした" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "このチャンネルを複製できませんでした" @@ -10511,6 +10541,11 @@ msgstr "友達リクエストを無視できませんでした。もう一度お msgid "Couldn't join community" msgstr "コミュニティに参加できませんでした" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "P2P通話に参加できませんでした。もう一度お試しください。" + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "請求ポータルを開けませんでした" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "この会話をピン留めできませんでした" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "コマンドを実行できませんでした: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "チャンネル設定を保存できませんでした" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "このチャンネルのフォローを解除できませんでした" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "この会話のピン留めを解除できませんでした" @@ -11374,7 +11409,7 @@ msgstr "カスタム" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "カスタム" @@ -11938,7 +11973,7 @@ msgstr "デフォルト" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "「{stickerName}」を削除しますか?この操作は元に戻せ #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "{channelLabel}を削除しますか?この操作は元に戻せません。" @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "デバイスの音声 ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "デバイスの音声のみ" @@ -12694,7 +12729,7 @@ msgstr "デバイスの詳細" msgid "Device revoked" msgstr "デバイスを取り消しました" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "デバイス選択" @@ -12758,6 +12793,11 @@ msgstr "サイズとメンバー数の違い" msgid "Dim strikethrough text" msgstr "取り消し線のテキストを薄く表示する" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "直接接続。メディアは参加者間で暗号化されます。" + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "DMサイドバー" msgid "DNS TXT record" msgstr "DNS TXTレコード" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "今後、確認しない" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "複製" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "チャンネルを複製" @@ -13825,7 +13870,7 @@ msgstr "カテゴリを編集" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "チャンネルを編集" @@ -15004,7 +15049,7 @@ msgstr "Escキー" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Escキーでキーボードモードを終了" @@ -15047,6 +15092,11 @@ msgstr "このチャンネルをフォローしているすべてのコミュニ msgid "Everyone" msgstr "全員" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "通話中の全員が無料でPlutoniumストリーム品質を利用できます:最大4Kソース解像度、60fps。" + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "{permissions} を付与しました" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "すべての権限を付与し、チャンネルごとの制限を無視します。非常に強力な権限です。" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "詳細選択" @@ -17417,7 +17467,7 @@ msgstr "ギリシャ語" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "グリッド表示" @@ -17909,7 +17959,7 @@ msgstr "参加者を非表示" msgid "Hide password" msgstr "パスワードを非表示" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "プレビューサムネイルを非表示" @@ -18322,11 +18372,11 @@ msgstr "無視" msgid "Ignore friend request" msgstr "友達リクエストを無視" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "ハードウェアデバイスを無視" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "入力メディアを無視" @@ -18357,7 +18407,7 @@ msgstr "{displayName}からの友達リクエストを無視しますか?" msgid "Ignore this channel's slowmode cooldown." msgstr "このチャンネルのスローモードのクールダウンを無視します。" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "仮想デバイスを無視" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "入力デバイス" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "入力レベル" @@ -18865,7 +18915,7 @@ msgstr "入力監視" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "入力プロファイル" @@ -19431,6 +19481,12 @@ msgstr "ジッター:" msgid "Join" msgstr "参加" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "参加する" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "コミュニティに参加" msgid "Join a community to unlock stickers." msgstr "コミュニティに参加してスタンプをアンロックしましょう。" +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "P2P通話に参加しますか?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "参加方法" msgid "Join on this device" msgstr "このデバイスで参加" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "P2P通話に確認なしで参加" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "「{threadName}」を退会しますか? これはプライベート msgid "Leave blank for a random username" msgstr "空欄にするとランダムなユーザー名が設定されます" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "通話から退出" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "デスクトップアプリの再起動後、カスタムCSSでウィンドウの透明な領域を表示します。" #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "他のコミュニティがこのチャンネルをフォローし、あなたが公開したメッセージのコピーを受け取れるようにします。" @@ -20729,6 +20797,11 @@ msgstr "ルイジアナ州" msgid "Low" msgstr "低" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "低遅延。" + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "ミシガン州" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "ミネソタ州" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "カメラを反転" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "私の素敵なスタンプ" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "自分のカテゴリ" @@ -24444,7 +24517,7 @@ msgstr "このサインインを自分で開始したデバイスでのみ続行 msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "クレジットカードのみご利用いただけます。デビットカードやプリペイドカードは、成人向けコンテンツの確認にはご利用いただけません。" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "デフォルトスピーカーのみ" @@ -24522,7 +24595,7 @@ msgstr "このチャンネルは、選択されたメンバーとロールのみ msgid "Only show message previews for DMs with unread messages" msgstr "未読メッセージがあるDMのメッセージプレビューのみ表示" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "スピーカーのみ" @@ -24759,7 +24832,7 @@ msgstr "{channelName}のチャンネル詳細を開く" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "チャットを開く" @@ -24840,7 +24913,7 @@ msgstr "ブラウザで絵文字を開く" msgid "Open emoji picker" msgstr "絵文字ピッカーを開く" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "イベントシンクを開く" @@ -25280,6 +25353,16 @@ msgstr "参加中の他のスレッド" msgid "Other devices" msgstr "その他のデバイス" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "他の参加者にIPアドレスが表示されます。" + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "P2P通話の他の参加者はあなたのIPアドレスを見ることができます。" + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "現在、<0>{username}に所有権を移行できませんでした msgid "Ownership transferred" msgstr "所有権を移行しました" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "現在、P2P通話は利用できません。" + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "P2P通話のみ" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "グループをピン留めしました" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "グループDMをピン留めしました" @@ -26536,7 +26635,7 @@ msgstr "プレビューボタン" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "カメラをプレビュー" @@ -27107,7 +27206,7 @@ msgstr "プッシュトゥトークキー" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "プッシュトゥトーク" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "メッセージを短くして、もう一度お試しください。" #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "トピックを短くして、もう一度お試しください。" @@ -31488,7 +31587,7 @@ msgstr "接続ごとの音量コントロールを表示" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "コンテキストメニューのショートカットを表示" @@ -31573,7 +31672,7 @@ msgstr "表示するサウンドエフェクトを減らす" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "チャット入力欄にフォーカスリングを表示" @@ -31701,7 +31800,7 @@ msgstr "ミュート中のチャンネルを表示" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "自分のカメラを表示" @@ -31742,7 +31841,7 @@ msgstr "新しい投稿を未読として表示" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "ビデオオフの参加者を表示" @@ -32144,7 +32243,7 @@ msgstr "サイレント着信" msgid "Silent calls" msgstr "サイレント着信" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "音声入力がない場合はミュート" @@ -32655,8 +32754,8 @@ msgstr "問題が発生しました。" #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "問題が発生しました。しばらくしてからもう一度お試しください。" @@ -32750,7 +32849,7 @@ msgstr "サウンド" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "ソース" @@ -32859,7 +32958,7 @@ msgstr "話し始めてください。終わったら「停止」を押してく #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "スピーカー" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "グループDMを開始" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "ビデオ通話を開始しますか?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "音声通話を開始しますか?" @@ -33092,6 +33191,16 @@ msgstr "開始日は終了日より前に設定してください。" msgid "Start drag and drop" msgstr "ドラッグ&ドロップを開始" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "P2Pモードで開始" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "P2Pモードで開始しますか?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "最小化して起動" msgid "Start over" msgstr "最初からやり直す" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "P2P通話を開始" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "検索の先頭に <0>{peoplePrefix} <1>{textChannelPrefix} <2>{ msgid "Start speaking" msgstr "読み上げを開始" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "標準通話を開始" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "ステータスパネル" msgid "Status:" msgstr "ステータス:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "残る" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "停止" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "アナウンスチャンネルを停止しますか?" @@ -33921,6 +34045,11 @@ msgstr "前のコミュニティまたはDMに切り替える" msgid "Switch to single column" msgstr "1列表示に切り替える" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "標準通話に切り替える" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "グループのピン留めを解除しました" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "グループDMのピン留めを解除しました" @@ -37727,6 +37856,11 @@ msgstr "未確認のメールアドレス" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "最大{DISCOVERY_MAX_TAGS}個のタグで、あなたのコミュニティを見つけやすくしましょう。これらのタグは「見つける」の検索結果に表示されます。" +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "最大{maxParticipants, plural, one {#人} other {#人}}まで" + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "絵文字をアップロード中" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "ビデオ送信:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "ビデオ設定" @@ -39570,7 +39704,7 @@ msgstr "このブラウザでは音声録音に対応していません。" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "ボイスリージョン" @@ -39582,7 +39716,7 @@ msgstr "音声セッション復元ナグバー" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "音声設定" diff --git a/fluxer_app/src/features/i18n/locales/ko/messages.po b/fluxer_app/src/features/i18n/locales/ko/messages.po index 49b82a728..ee78b8fc6 100644 --- a/fluxer_app/src/features/i18n/locales/ko/messages.po +++ b/fluxer_app/src/features/i18n/locales/ko/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {#개 차단된 메시지} other {#개 차단된 메시지}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {#개의 채널이 이 채널을 구독 중입니다} other {#개의 채널이 이 채널을 구독 중입니다}} 이 채널을 텍스트 채널로 전환하면 해당 구독이 제거됩니다." @@ -2720,6 +2720,11 @@ msgstr "기본 이모지입니다. {productName} 어디에서든 사용할 수 msgid "A description is required." msgstr "설명이 필요합니다." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "직접 연결할 수 없습니다. 서로 듣거나 볼 수 없습니다." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "리치 임베드 또는 링크 미리보기(업로드된 파일 아님)" msgid "A separate profile for each community" msgstr "커뮤니티별 별도 프로필" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "일반 통화는 서버를 통해 이루어지며 모든 사용자에게 작동합니다." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "관리자가 초대를 일시 중지했습니다. 지금은 참여할 #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "누구나 계정을 요청할 수 있지만, 관리자의 승인이 필 msgid "Anyone in my communities" msgstr "내 커뮤니티에 있는 모든 사람" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "직접 연결할 수 없는 사람은 누군가가 통화를 일반 통화로 전환할 때까지 참여할 수 없습니다." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "찾는 사람이 요청을 보낼 수 있습니다. 아래 그룹을 선택하려면 이 기능을 끄세요." @@ -5112,6 +5127,16 @@ msgstr "오디오" msgid "Audio & video" msgstr "오디오 및 비디오" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "오디오 및 비디오는 참가자 간에 직접 전송되며, 최대 {maxParticipants, plural, one {#명} other {#명}}까지 가능합니다. 모든 참가자는 참여 전에 동의하며 서로의 IP 주소를 볼 수 있습니다." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "오디오 및 비디오는 참가자 간에 직접 전송됩니다." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "자동 완성 제안: {label}, {resultCount}개 중 {selectedOptionPosit #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "자동" @@ -6936,15 +6961,15 @@ msgstr "계정을 삭제할 수 없습니다" msgid "Capture" msgstr "캡처" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "앱 오디오 캡처" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "데스크톱 오디오 캡처" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "기기 오디오 캡처" @@ -7017,7 +7042,7 @@ msgstr "카테고리 ID 복사됨" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "카테고리 이름" @@ -7554,7 +7579,7 @@ msgstr "채널 정보 섹션" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "채널 ID 복사됨" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "채널 링크" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "채널 링크 복사됨" @@ -7604,7 +7629,7 @@ msgstr "채널 관리" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "채널이 상위 카테고리와 동기화되었습니다" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "채널 주제가 너무 깁니다." @@ -7668,8 +7693,8 @@ msgstr "채널 유형 선택" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "채널 업데이트됨" @@ -9421,7 +9446,7 @@ msgstr "음성 채널 참여 전 확인" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "통화 시작 전 확인" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "대화" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "변환" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "채널을 공지 채널로 전환했습니다" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "이 채널을 텍스트 채널로 전환하면 팔로우하는 모든 채널이 삭제됩니다." @@ -9949,7 +9974,7 @@ msgstr "채널 ID 복사" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "채널 링크 복사" @@ -10379,13 +10404,18 @@ msgstr "로그인을 완료할 수 없습니다. 다시 시도해 주세요." msgid "Couldn't connect to {domain}. Try again." msgstr "{domain}(으)로 연결할 수 없습니다. 다시 시도해 보세요." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "{names}(님)에게 연결할 수 없습니다." + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "음성 연결에 실패했습니다. 다시 시도해 주세요." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "채널을 변환할 수 없습니다" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "데스크톱 업데이트를 다운로드할 수 없습니다" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "채널을 복제할 수 없습니다" @@ -10511,6 +10541,11 @@ msgstr "친구 요청을 무시할 수 없습니다. 다시 시도해 주세요. msgid "Couldn't join community" msgstr "커뮤니티에 참여할 수 없습니다" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "P2P 통화에 참여할 수 없습니다. 다시 시도해 주세요." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "결제 포털을 열 수 없습니다" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "이 대화를 고정할 수 없습니다" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "명령을 실행할 수 없습니다: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "채널 설정을 저장할 수 없습니다" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "이 채널을 구독 취소할 수 없습니다" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "이 대화를 고정 해제할 수 없습니다" @@ -11374,7 +11409,7 @@ msgstr "사용자 지정" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "사용자 지정" @@ -11938,7 +11973,7 @@ msgstr "기본" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "\"{stickerName}\"을(를) 삭제하시겠어요? 이 작업은 되돌릴 #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "{channelLabel}을(를) 삭제하시겠어요? 이 작업은 되돌릴 수 없습니다." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "기기 오디오({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "기기 오디오만" @@ -12694,7 +12729,7 @@ msgstr "기기 정보" msgid "Device revoked" msgstr "기기 연결 해제됨" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "기기 선택" @@ -12758,6 +12793,11 @@ msgstr "다양한 크기 및 멤버 수" msgid "Dim strikethrough text" msgstr "취소선 텍스트 흐리게 표시" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "직접 연결. 참여자 간 미디어가 암호화됩니다." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "DM 사이드바" msgid "DNS TXT record" msgstr "DNS TXT 레코드" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "다시 묻지 않기" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "복제" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "채널 복제" @@ -13825,7 +13870,7 @@ msgstr "카테고리 편집" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "채널 편집" @@ -15004,7 +15049,7 @@ msgstr "Esc 키" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Esc 키로 키보드 모드 종료" @@ -15047,6 +15092,11 @@ msgstr "이 채널을 구독하는 모든 커뮤니티에 복사본이 전송됩 msgid "Everyone" msgstr "모든 사람" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "통화에 참여한 모든 분께는 무료로 플루토늄 스트림 품질이 제공됩니다. 최대 4K 소스 해상도 및 60fps를 지원합니다." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "{permissions} 권한을 부여했습니다" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "모든 권한을 부여하고 채널 제한을 우회합니다. 매우 민감한 권한입니다." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "세부 선택" @@ -17417,7 +17467,7 @@ msgstr "그리스어" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "격자 보기" @@ -17909,7 +17959,7 @@ msgstr "참여자 숨기기" msgid "Hide password" msgstr "비밀번호 숨기기" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "미리보기 썸네일 숨기기" @@ -18322,11 +18372,11 @@ msgstr "무시" msgid "Ignore friend request" msgstr "친구 요청 무시하기" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "하드웨어 기기 무시" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "입력 미디어 무시" @@ -18357,7 +18407,7 @@ msgstr "{displayName}님의 친구 요청을 무시하시겠어요?" msgid "Ignore this channel's slowmode cooldown." msgstr "이 채널의 슬로우 모드 제한을 무시합니다." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "가상 기기 무시" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "입력 장치" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "입력 레벨" @@ -18865,7 +18915,7 @@ msgstr "입력 모니터링" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "입력 프로필" @@ -19431,6 +19481,12 @@ msgstr "지터:" msgid "Join" msgstr "참여하기" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "참여" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "커뮤니티 참여하기" msgid "Join a community to unlock stickers." msgstr "스티커를 사용하려면 커뮤니티에 참여하세요." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "P2P 통화에 참여하시겠어요?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "참여 방법" msgid "Join on this device" msgstr "이 기기에서 참여" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "P2P 통화 시 확인 없이 바로 참여" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "{threadName}에서 나가시겠어요? 이곳은 비공개 스레드이 msgid "Leave blank for a random username" msgstr "비워두시면 임의의 사용자 이름이 생성됩니다" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "통화 나가기" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "데스크톱 앱 재시작 시 사용자 지정 CSS가 투명한 창 영역을 표시하도록 허용합니다." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "다른 커뮤니티에서 이 채널을 팔로우하고 게시하는 내용의 복사본을 받을 수 있습니다." @@ -20729,6 +20797,11 @@ msgstr "루이지애나" msgid "Low" msgstr "낮음" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "낮은 지연 시간." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "미시간" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "미네소타" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "카메라 좌우 반전" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "내 멋진 스티커" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "내 카테고리" @@ -24444,7 +24517,7 @@ msgstr "이 로그인은 본인이 제어하는 기기에서 시작한 것이 msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "신용카드만 사용할 수 있습니다. 성인 콘텐츠 확인을 위해 직불카드 및 선불카드는 사용할 수 없습니다." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "기본 스피커만" @@ -24522,7 +24595,7 @@ msgstr "선택한 멤버와 역할만 이 채널을 볼 수 있습니다." msgid "Only show message previews for DMs with unread messages" msgstr "읽지 않은 메시지가 있는 DM에 대해서만 메시지 미리보기 표시" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "스피커만" @@ -24759,7 +24832,7 @@ msgstr "{channelName} 채널 정보 열기" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "채팅 열기" @@ -24840,7 +24913,7 @@ msgstr "브라우저에서 이모티콘 열기" msgid "Open emoji picker" msgstr "이모티콘 선택기 열기" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "이벤트 싱크 열기" @@ -25280,6 +25353,16 @@ msgstr "다른 활성 스레드" msgid "Other devices" msgstr "다른 기기" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "다른 참여자가 IP 주소를 볼 수 있습니다." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "P2P 통화의 다른 참가자가 내 IP 주소를 볼 수 있습니다." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "현재 <0>{username}님에게 소유권을 이전할 수 없습니 msgid "Ownership transferred" msgstr "소유권이 이전되었습니다" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "지금은 P2P 통화를 사용할 수 없습니다." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "P2P 통화만" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "고정된 그룹" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "고정된 그룹 DM" @@ -26536,7 +26635,7 @@ msgstr "미리보기 버튼" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "카메라 미리보기" @@ -27107,7 +27206,7 @@ msgstr "눌러서 말하기 키" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "눌러서 말하기" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "메시지를 줄여서 다시 시도해 주세요." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "주제를 줄여서 다시 시도해 주세요." @@ -31488,7 +31587,7 @@ msgstr "연결 볼륨 컨트롤 표시" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "상황별 메뉴 단축키 표시" @@ -31573,7 +31672,7 @@ msgstr "효과음 적게 보기" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "채팅 입력창에 포커스 링 표시" @@ -31701,7 +31800,7 @@ msgstr "음소거한 채널 표시" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "내 카메라 표시" @@ -31742,7 +31841,7 @@ msgstr "새 글을 읽지 않음으로 표시" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "비디오를 켜지 않은 참여자 표시" @@ -32144,7 +32243,7 @@ msgstr "무음 통화" msgid "Silent calls" msgstr "무음 통화" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "오디오 입력이 없는 캡처 장치는 음소거됩니다" @@ -32655,8 +32754,8 @@ msgstr "문제가 발생했습니다." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "문제가 발생했습니다. 잠시 후 다시 시도해 주세요." @@ -32750,7 +32849,7 @@ msgstr "소리" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "원본" @@ -32859,7 +32958,7 @@ msgstr "지금 말하세요. 완료되면 중지를 누르세요. 나중에 다 #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "스피커" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "그룹 DM 시작" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "영상 통화를 시작할까요?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "음성 통화를 시작할까요?" @@ -33092,6 +33191,16 @@ msgstr "시작일은 종료일보다 빨라야 합니다." msgid "Start drag and drop" msgstr "끌어서 놓기 시작" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "P2P 모드로 시작" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "P2P 모드로 시작할까요?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "최소화하여 시작" msgid "Start over" msgstr "다시 시작" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "P2P 통화 시작" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "검색어 앞에 <0>{peoplePrefix}, <1>{textChannelPrefix}, <2>{ msgid "Start speaking" msgstr "말하기 시작" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "표준 통화 시작" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "상태 패널" msgid "Status:" msgstr "상태:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "유지" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "중지" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "공지 채널 사용을 중지하시겠어요?" @@ -33921,6 +34045,11 @@ msgstr "이전 커뮤니티 또는 DM으로 전환" msgid "Switch to single column" msgstr "1열로 전환" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "일반 통화로 전환" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "고정 해제된 그룹" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "고정 해제된 그룹 DM" @@ -37727,6 +37856,11 @@ msgstr "인증되지 않은 이메일" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "최대 {DISCOVERY_MAX_TAGS}개의 태그로 커뮤니티를 더 쉽게 찾을 수 있습니다. 이 태그는 디스커버리 검색에 표시됩니다." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "최대 {maxParticipants, plural, one {#명} other {#명}}까지" + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "이모티콘 업로드 중" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "비디오 전송:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "비디오 설정" @@ -39570,7 +39704,7 @@ msgstr "이 브라우저에서는 음성 녹음을 지원하지 않습니다." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "음성 지역" @@ -39582,7 +39716,7 @@ msgstr "음성 세션 복원 알림 표시줄" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "음성 설정" diff --git a/fluxer_app/src/features/i18n/locales/lt/messages.po b/fluxer_app/src/features/i18n/locales/lt/messages.po index 95836c10a..5459b0d05 100644 --- a/fluxer_app/src/features/i18n/locales/lt/messages.po +++ b/fluxer_app/src/features/i18n/locales/lt/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# užblokuota žinutė} few {# užblokuotos žinutės} other {# užblokuotų žinučių}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# kanalas jį seka} other {# kanalai jį seka}} šį kanalą. Konvertavus jį į tekstinį kanalą, tie sekimai bus pašalinti." @@ -2720,6 +2720,11 @@ msgstr "Numylėtas jaustukas. Galite jį naudoti bet kurioje „{productName}“ msgid "A description is required." msgstr "Aprašymas yra privalomas." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Nepavyko užmegzti tiesioginio ryšio. Negalite girdėti ar matyti vienas kito." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "raiškusis įterptasis elementas arba nuorodos peržiūra (ne įkeltas f msgid "A separate profile for each community" msgstr "Atskiras profilis kiekvienai bendruomenei" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Įprastas skambutis vyksta per serverį ir veikia visiems." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Administratoriai pristabdė kvietimus – šiuo metu negalite prisijungt #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Visi gali paprašyti paskyros, bet administratorius turi ją patvirtinti msgid "Anyone in my communities" msgstr "Visi mano bendruomenėse" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Jei kas nors negali prisijungti tiesiogiai, negalės dalyvauti, kol kas nors nepereis prie standartinio skambučio." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Visi, kas jus suras, gali atsiųsti užklausą. Išjunkite tai, kad pasirinktumėte toliau esančias grupes." @@ -5112,6 +5127,16 @@ msgstr "Garsas" msgid "Audio & video" msgstr "Garsas ir vaizdas" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Garso ir vaizdo įrašai siunčiami tiesiogiai tarp dalyvių, iki {maxParticipants, plural, one {# asmuo} other {# žmonės}}. Visi sutinka prieš prisijungdami ir gali matyti vieni kitų IP adresus." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Garsas ir vaizdas siunčiami tiesiogiai tarp dalyvių." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Automatinio užbaigimo pasiūlymas: {label}, {selectedOptionPosition} i #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatinis" @@ -6936,15 +6961,15 @@ msgstr "Negalima ištrinti paskyros" msgid "Capture" msgstr "Įrašyti" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Įrašyti programos garsą" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Įrašyti darbalaukio garsą" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Įrašyti įrenginio garsą" @@ -7017,7 +7042,7 @@ msgstr "Kategorijos ID nukopijuotas" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Kategorijos pavadinimas" @@ -7554,7 +7579,7 @@ msgstr "Kanalo informacijos skiltys" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Kanalo ID nukopijuotas" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "kanalo nuoroda" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Kanalo nuoroda nukopijuota" @@ -7604,7 +7629,7 @@ msgstr "Kanalų valdymas" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Kanalas sinchronizuotas su pagrindine kategorija" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Kanalo tema per ilga." @@ -7668,8 +7693,8 @@ msgstr "Kanalo tipo pasirinkimas" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Kanalas atnaujintas" @@ -9421,7 +9446,7 @@ msgstr "Patvirtinti prieš prisijungiant prie balso kanalų" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Patvirtinti prieš pradedant skambučius" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Pokalbiai" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Konvertuoti" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Kanalas konvertuotas į skelbimo kanalą" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Konvertavus šį kanalą į tekstinį, bus pašalinti visi jį sekantys kanalai." @@ -9949,7 +9974,7 @@ msgstr "Kopijuoti kanalo ID" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Kopijuoti kanalo nuorodą" @@ -10379,13 +10404,18 @@ msgstr "Nepavyko prisijungti. Bandykite dar kartą." msgid "Couldn't connect to {domain}. Try again." msgstr "Nepavyko prisijungti prie {domain}. Pabandykite dar kartą." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Nepavyko prisijungti prie {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Nepavyko prisijungti prie balso pokalbio. Pabandykite dar kartą." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Nepavyko konvertuoti šio kanalo" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Nepavyko atsisiųsti darbalaukio naujinimo" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Nepavyko dubliuoti šio kanalo" @@ -10511,6 +10541,11 @@ msgstr "Nepavyko ignoruoti draugystės užklausos. Bandykite dar kartą." msgid "Couldn't join community" msgstr "Nepavyko prisijungti prie bendruomenės" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Nepavyko prisijungti prie P2P skambučio. Pabandykite dar kartą." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Nepavyko atidaryti atsiskaitymo portalo" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Nepavyko prisegti šio pokalbio" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Nepavyko įvykdyti komandos: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Nepavyko įrašyti kanalo nustatymų" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Nepavyko atsisakyti šio kanalo stebėjimo" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Nepavyko atsegti šio pokalbio" @@ -11374,7 +11409,7 @@ msgstr "Pasirinktinis šaltinis" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Pasirinktinis" @@ -11938,7 +11973,7 @@ msgstr "Numatytasis" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Ištrinti „{stickerName}“? Šio veiksmo anuliuoti negalima." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Ištrinti {channelLabel}? Šio veiksmo anuliuoti negalima." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Įrenginio garsas ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Tik garso įrenginys" @@ -12694,7 +12729,7 @@ msgstr "Įrenginio informacija" msgid "Device revoked" msgstr "Įrenginys atšauktas" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Įrenginio pasirinkimas" @@ -12758,6 +12793,11 @@ msgstr "Įvairūs dydžiai ir narių skaičiai" msgid "Dim strikethrough text" msgstr "Pritemdyti perbrauktą tekstą" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Tiesioginis ryšys. Medija šifruojama tarp dalyvių." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "DM šoninė juosta" msgid "DNS TXT record" msgstr "DNS TXT įrašas" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Daugiau neklausti, visada sutikti" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Dubliuoti" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Dubliuoti kanalą" @@ -13825,7 +13870,7 @@ msgstr "Redaguoti kategoriją" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Redaguoti kanalą" @@ -15004,7 +15049,7 @@ msgstr "Esc klavišas" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Klavišu Esc išeinama iš klaviatūros režimo" @@ -15047,6 +15092,11 @@ msgstr "Kiekviena šį kanalą stebinti bendruomenė gaus kopiją. Jei vėliau j msgid "Everyone" msgstr "Visi" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Visi skambučio dalyviai nemokamai gauna „Plutonium“ transliacijos kokybę: iki 4K raiškos ir 60 kadrų per sekundę." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Suteikta {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Suteikia visus leidimus ir apeina kanalo apribojimus. Labai jautrus leidimas." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Detalus pasirinkimas" @@ -17417,7 +17467,7 @@ msgstr "graikų" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Tinklelio rodinys" @@ -17910,7 +17960,7 @@ msgstr "Slėpti dalyvius" msgid "Hide password" msgstr "Slėpti slaptažodį" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Slėpti peržiūros miniatiūrą" @@ -18323,11 +18373,11 @@ msgstr "Ignoruoti" msgid "Ignore friend request" msgstr "Nepaisyti draugystės užklausos" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignoruoti aparatinius įrenginius" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignoruoti įvesties mediją" @@ -18358,7 +18408,7 @@ msgstr "Nepaisyti {displayName} draugystės užklausos?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignoruoti šio kanalo lėtojo režimo apribojimą." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignoruoti virtualius" @@ -18854,7 +18904,7 @@ msgid "Input device" msgstr "Įvesties įrenginys" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Įvesties lygis" @@ -18866,7 +18916,7 @@ msgstr "Įvesties stebėjimas" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Įvesties profilis" @@ -19432,6 +19482,12 @@ msgstr "Delsos svyravimas:" msgid "Join" msgstr "Prisijungti" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Prisijungti" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19456,6 +19512,11 @@ msgstr "Prisijungti prie bendruomenės" msgid "Join a community to unlock stickers." msgstr "Prisijunkite prie bendruomenės, kad atrakintumėte lipdukus." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Prisijungti prie P2P skambučio?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19494,6 +19555,11 @@ msgstr "Prisijungimo būdas" msgid "Join on this device" msgstr "Prisijungti šiuo įrenginiu" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Prisijungti prie P2P skambučių be patvirtinimo" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20024,8 +20090,10 @@ msgstr "Palikti {threadName}? Tai privatu, todėl galėsite grįžti tik tada, j msgid "Leave blank for a random username" msgstr "Palikite tuščią, kad būtų sugeneruotas atsitiktinis naudotojo vardas" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Išeiti iš skambučio" @@ -20202,7 +20270,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Leidžia pasirinktiniam CSS atskleisti permatomas lango sritis iš naujo paleidus darbalaukio programą." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Leidžia kitiems bendruomenėms sekti šį kanalą ir gauti jūsų paskelbtų žinučių kopijas." @@ -20730,6 +20798,11 @@ msgstr "Luiziana" msgid "Low" msgstr "Žemas" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Mažesnis delsos laikas." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22004,7 +22077,7 @@ msgstr "Mičiganas" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22102,7 +22175,7 @@ msgstr "Minesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Veidrodinis kameros vaizdas" @@ -22824,7 +22897,7 @@ msgid "My awesome sticker" msgstr "Mano nuostabus lipdukas" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Mano kategorija" @@ -24445,7 +24518,7 @@ msgstr "Tęskite tik tuo atveju, jei šį prisijungimą pradėjote valdydami sav msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Priimamos tik kredito kortelės. Debeto ir išankstinio mokėjimo kortelės negali būti naudojamos turinio suaugusiesiems patikrai." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Tik numatytieji garsiakalbiai" @@ -24523,7 +24596,7 @@ msgstr "Šį kanalą matys tik pasirinkti nariai ir vaidmenys." msgid "Only show message previews for DMs with unread messages" msgstr "Rodyti žinučių peržiūras tik DM su neskaitytomis žinutėmis" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Tik garsiakalbiai" @@ -24760,7 +24833,7 @@ msgstr "Atidaryti kanalo „{channelName}“ išsamią informaciją" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Atidaryti pokalbį" @@ -24841,7 +24914,7 @@ msgstr "Atidaryti jaustuką naršyklėje" msgid "Open emoji picker" msgstr "Atidaryti jaustukų parinkiklį" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Atidaryti įvykių rinktuvą" @@ -25281,6 +25354,16 @@ msgstr "Kitos aktyvios temų grupės" msgid "Other devices" msgstr "Kiti įrenginiai" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Kiti dalyviai gali matyti jūsų IP adresą." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Kiti P2P skambučio dalyviai gali matyti jūsų IP adresą." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25396,6 +25479,22 @@ msgstr "Šiuo metu nepavyko perduoti nuosavybės naudotojui <0>{username}." msgid "Ownership transferred" msgstr "Nuosavybė perduota" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Šiuo metu P2P skambučiai nepasiekiami." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Tik P2P skambučiai" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26097,7 +26196,7 @@ msgstr "Prisegta grupė" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Prisegtas grupės DM" @@ -26537,7 +26636,7 @@ msgstr "Peržiūros mygtukas" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Peržiūrėti kamerą" @@ -27108,7 +27207,7 @@ msgstr "Kalbėjimo paspaudus klavišas" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Kalbėjimas paspaudus" @@ -31389,7 +31488,7 @@ msgid "Shorten the message and try again." msgstr "Sutrumpinkite žinutę ir bandykite dar kartą." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Sutrumpinkite temą ir bandykite dar kartą." @@ -31489,7 +31588,7 @@ msgstr "Rodyti ryšio garsumo valdiklius" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Rodyti kontekstinio meniu sparčiuosius klavišus" @@ -31574,7 +31673,7 @@ msgstr "Rodyti mažiau garso efektų" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Rodyti fokusavimo žiedą pokalbio teksto laukelyje" @@ -31702,7 +31801,7 @@ msgstr "Rodyti nutildytus kanalus" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Rodyti mano kamerą" @@ -31743,7 +31842,7 @@ msgstr "Rodyti naujus įrašus kaip neskaitytus" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Rodyti dalyvius be vaizdo" @@ -32145,7 +32244,7 @@ msgstr "Tylus skambutis" msgid "Silent calls" msgstr "Tylūs skambučiai" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Nebalsinis, kai įrenginys neturi savo garso įvesties" @@ -32656,8 +32755,8 @@ msgstr "Kažkas nepavyko." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Kažkas nepavyko. Bandykite dar kartą po akimirkos." @@ -32751,7 +32850,7 @@ msgstr "Garsai" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Šaltinis" @@ -32860,7 +32959,7 @@ msgstr "Kalbėkite dabar. Baigę paspauskite „Stabdyti“ – vėliau galėsit #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Garsiakalbis" @@ -33058,12 +33157,12 @@ msgid "Start a group DM" msgstr "Pradėti grupės DM" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Pradėti vaizdo skambutį?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Pradėti balso skambutį?" @@ -33093,6 +33192,16 @@ msgstr "Pradžios data turi būti ankstesnė už pabaigos datą." msgid "Start drag and drop" msgstr "Pradėti vilkti ir mesti" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Pradėti P2P režimu" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Pradėti P2P režimu?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33118,6 +33227,11 @@ msgstr "Pradėti sumažintą" msgid "Start over" msgstr "Pradėti iš naujo" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Pradėti P2P skambutį" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33132,6 +33246,11 @@ msgstr "Pradėkite paiešką nuo <0>{peoplePrefix} <1>{textChannelPrefix}{username}{peoplePrefix} <1>{textChannelPrefix}{username} akkurat nå." msgid "Ownership transferred" msgstr "Eierskap overført" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "P2P-anrop er utilgjengelige akkurat nå." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Kun P2P-anrop" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Festet gruppechat" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Festet gruppechat" @@ -26536,7 +26635,7 @@ msgstr "Forhåndsvisningsknapp" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Forhåndsvis kamera" @@ -27107,7 +27206,7 @@ msgstr "Tast for trykk for å snakke" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Trykk for å snakke" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Forkort meldingen og prøv igjen." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Kort ned emnet og prøv igjen." @@ -31488,7 +31587,7 @@ msgstr "Vis volumkontroller for tilkobling" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Vis snarveier for hurtigmeny" @@ -31573,7 +31672,7 @@ msgstr "Vis færre lydeffekter" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Vis fokusring på chattefeltet" @@ -31701,7 +31800,7 @@ msgstr "Vis dempede kanaler" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Vis mitt eget kamera" @@ -31742,7 +31841,7 @@ msgstr "Vis nye innlegg som uleste" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Vis deltakere uten video" @@ -32144,7 +32243,7 @@ msgstr "Stille anrop" msgid "Silent calls" msgstr "Stille anrop" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Stille når opptaksenheten ikke har egen lydinngang" @@ -32655,8 +32754,8 @@ msgstr "Noe gikk galt." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Noe gikk galt. Prøv igjen om et øyeblikk." @@ -32750,7 +32849,7 @@ msgstr "Lyder" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Kilde" @@ -32859,7 +32958,7 @@ msgstr "Snakk nå. Trykk på Stopp når du er ferdig – du kan trimme etterpå. #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Høyttaler" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Start en gruppechat" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Starte et videoanrop?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Starte et taleanrop?" @@ -33092,6 +33191,16 @@ msgstr "Startdato må være tidligere enn sluttdato." msgid "Start drag and drop" msgstr "Start dra-og-slipp" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Start i P2P-modus" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Starte i P2P-modus?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Start minimert" msgid "Start over" msgstr "Start på nytt" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Start P2P-anrop" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Start søk med <0>{peoplePrefix} <1>{textChannelPrefix} <2>{voic msgid "Start speaking" msgstr "Start opplesing" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Start standardanrop" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Statuspanel" msgid "Status:" msgstr "Status:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Bli" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Stopp" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Slutte å være en kunngjøringskanal?" @@ -33921,6 +34045,11 @@ msgstr "Bytt til forrige fellesskap eller direktemeldinger" msgid "Switch to single column" msgstr "Bytt til én kolonne" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Bytt til standardanrop" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Løsnet gruppechat" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Løsnet gruppechat" @@ -37727,6 +37856,11 @@ msgstr "Ubekreftet e-post" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Opptil {DISCOVERY_MAX_TAGS} tagger hjelper folk med å finne fellesskapet ditt. De vises i søket i Oppdag." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Opptil {maxParticipants, plural, one {# person} other {# personer}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Laster opp emojier" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Sendt video:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Videoinnstillinger" @@ -39570,7 +39704,7 @@ msgstr "Taleopptak støttes ikke i denne nettleseren." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Taleregion" @@ -39582,7 +39716,7 @@ msgstr "Varsellinje for gjenoppretting av taleøkt" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Taleinnstillinger" diff --git a/fluxer_app/src/features/i18n/locales/pl/messages.po b/fluxer_app/src/features/i18n/locales/pl/messages.po index e763a3fa5..c7e5e11a6 100644 --- a/fluxer_app/src/features/i18n/locales/pl/messages.po +++ b/fluxer_app/src/features/i18n/locales/pl/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# zablokowana wiadomość} few {# zablokowane wiadomości} many {# zablokowanych wiadomości} other {# zablokowanych wiadomości}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# kanał obserwuje} other {# kanałów obserwuje}} ten kanał. Konwersja na kanał tekstowy usunie te obserwacje." @@ -2720,6 +2720,11 @@ msgstr "Domyślne emoji. Możesz go używać wszędzie w {productName}." msgid "A description is required." msgstr "Opis jest wymagany." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Nie udało się nawiązać bezpośredniego połączenia. Nie możecie się słyszeć ani widzieć." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "rozbudowana osadzona treść lub podgląd linku (nie przesłany plik)" msgid "A separate profile for each community" msgstr "Osobny profil dla każdej społeczności" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Zwykłe połączenie przechodzi przez serwer i działa u każdego." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Administratorzy wstrzymali zaproszenia — nie możesz teraz dołączyć #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Każdy może poprosić o konto, ale administrator musi je zatwierdzić." msgid "Anyone in my communities" msgstr "Każdy z moich społeczności" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Każdy, kto nie może połączyć się bezpośrednio, nie może wziąć udziału, dopóki ktoś nie przełączy rozmowy na standardową." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Każdy, kto Cię znajdzie, może wysłać prośbę. Wyłącz to, aby wybrać grupy poniżej." @@ -5112,6 +5127,16 @@ msgstr "Dźwięk" msgid "Audio & video" msgstr "Dźwięk i wideo" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Dźwięk i obraz przesyłane są bezpośrednio między uczestnikami, maksymalnie do {maxParticipants, plural, one {# osoby} other {# osób}}. Wszyscy zgadzają się przed dołączeniem i mogą widzieć swoje adresy IP." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Dźwięk i obraz przesyłane są bezpośrednio między uczestnikami." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Sugestia autouzupełniania: {label}, {selectedOptionPosition} z {resultC #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatycznie" @@ -6936,15 +6961,15 @@ msgstr "Nie można usunąć konta" msgid "Capture" msgstr "Przechwytywanie" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Przechwytuj dźwięk aplikacji" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Przechwytuj dźwięk komputera" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Przechwytuj dźwięk urządzenia" @@ -7017,7 +7042,7 @@ msgstr "Skopiowano identyfikator kategorii" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Nazwa kategorii" @@ -7554,7 +7579,7 @@ msgstr "Sekcje szczegółów kanału" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Skopiowano ID kanału" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "link do kanału" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Link do kanału skopiowany" @@ -7604,7 +7629,7 @@ msgstr "Zarządzanie kanałami" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Kanał zsynchronizowany z kategorią nadrzędną" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Temat kanału jest za długi." @@ -7668,8 +7693,8 @@ msgstr "Wybór typu kanału" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Kanał zaktualizowany" @@ -9421,7 +9446,7 @@ msgstr "Pytaj o potwierdzenie przed dołączeniem do kanałów głosowych" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Pytaj przed rozpoczęciem połączeń" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Rozmowy" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Konwertuj" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Przekształcono kanał w kanał ogłoszeń" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Konwersja tego kanału na kanał tekstowy spowoduje usunięcie wszystkich kanałów, które go obserwują." @@ -9949,7 +9974,7 @@ msgstr "Kopiuj ID kanału" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Kopiuj link do kanału" @@ -10379,13 +10404,18 @@ msgstr "Nie udało się dokończyć logowania. Spróbuj ponownie." msgid "Couldn't connect to {domain}. Try again." msgstr "Nie udało się połączyć z {domain}. Spróbuj ponownie." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Nie udało się połączyć z {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Nie udało się dołączyć do rozmowy głosowej. Spróbuj ponownie." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Nie można przekonwertować tego kanału" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Nie udało się pobrać aktualizacji aplikacji komputerowej" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Nie udało się zduplikować tego kanału" @@ -10511,6 +10541,11 @@ msgstr "Nie udało się zignorować zaproszenia do znajomych. Spróbuj ponownie. msgid "Couldn't join community" msgstr "Nie udało się dołączyć do społeczności" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Nie udało się dołączyć do rozmowy P2P. Spróbuj ponownie." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Nie udało się otworzyć portalu płatności" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Nie udało się przypiąć tej rozmowy" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Nie udało się uruchomić polecenia: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Nie można zapisać ustawień kanału" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Nie udało się anulować obserwowania tego kanału" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Nie można odpiąć tej rozmowy" @@ -11374,7 +11409,7 @@ msgstr "Niestandardowe źródło" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Niestandardowe" @@ -11938,7 +11973,7 @@ msgstr "Domyślne" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Usunąć \"{stickerName}\"? Tej operacji nie można cofnąć." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Usunąć {channelLabel}? Tej operacji nie można cofnąć." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Dźwięk urządzenia ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Tylko dźwięk z urządzenia" @@ -12694,7 +12729,7 @@ msgstr "Szczegóły urządzenia" msgid "Device revoked" msgstr "Cofnięto dostęp urządzenia" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Wybór urządzenia" @@ -12758,6 +12793,11 @@ msgstr "Różne rozmiary i liczby członków" msgid "Dim strikethrough text" msgstr "Przyciemnij przekreślony tekst" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Bezpośrednie połączenie. Multimedia są szyfrowane między uczestnikami." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Pasek boczny DM" msgid "DNS TXT record" msgstr "Rekord DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Nie pytaj ponownie, zawsze zgadzaj się" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplikuj" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplikuj kanał" @@ -13825,7 +13870,7 @@ msgstr "Edytuj kategorię" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Edytuj kanał" @@ -15004,7 +15049,7 @@ msgstr "Klawisz Esc" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Naciśnięcie klawisza Esc wyłącza tryb klawiatury" @@ -15047,6 +15092,11 @@ msgstr "Każda obserwująca ten kanał społeczność otrzyma kopię. Jeśli pó msgid "Everyone" msgstr "Wszyscy" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Wszyscy w rozmowie otrzymują bezpłatnie jakość strumienia Plutonium: do rozdzielczości źródłowej 4K i 60 kl./s." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Przyznano {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Przyznaje wszystkie uprawnienia i pozwala omijać ograniczenia kanałów. Wymaga szczególnej ostrożności." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Szczegółowy wybór" @@ -17417,7 +17467,7 @@ msgstr "Grecki" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Widok siatki" @@ -17909,7 +17959,7 @@ msgstr "Ukryj uczestników" msgid "Hide password" msgstr "Ukryj hasło" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Ukryj miniaturę podglądu" @@ -18322,11 +18372,11 @@ msgstr "Ignoruj" msgid "Ignore friend request" msgstr "Ignoruj zaproszenie do znajomych" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignoruj urządzenia sprzętowe" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignoruj multimedia wejściowe" @@ -18357,7 +18407,7 @@ msgstr "Zignorować zaproszenie do znajomych od {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Pomijaj czas oczekiwania między wiadomościami w trybie wolnym tego kanału." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignoruj wirtualne" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Urządzenie wejściowe" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Poziom wejścia" @@ -18865,7 +18915,7 @@ msgstr "Monitorowanie danych wejściowych" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Profil wejścia" @@ -19431,6 +19481,12 @@ msgstr "Jitter:" msgid "Join" msgstr "Dołącz" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Dołącz" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Dołącz do społeczności" msgid "Join a community to unlock stickers." msgstr "Dołącz do społeczności, aby odblokować naklejki." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Dołączyć do rozmowy P2P?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Metoda dołączenia" msgid "Join on this device" msgstr "Dołącz na tym urządzeniu" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Dołączaj do rozmów P2P bez potwierdzenia" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Opuścić wątek „{threadName}”? Jest prywatny, więc możesz wróci msgid "Leave blank for a random username" msgstr "Pozostaw puste, aby użyć losowej nazwy użytkownika" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Opuść rozmowę" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Pozwala niestandardowemu CSS na wyświetlanie przezroczystych obszarów okna po ponownym uruchomieniu aplikacji komputerowej." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Pozwól innym społecznościom śledzić ten kanał i otrzymywać kopie publikowanych przez Ciebie wiadomości." @@ -20729,6 +20797,11 @@ msgstr "Luizjana" msgid "Low" msgstr "Niski" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Niższe opóźnienia." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Odbij obraz z kamery" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Moja wspaniała naklejka" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Moja kategoria" @@ -24444,7 +24517,7 @@ msgstr "Kontynuuj tylko wtedy, gdy rozpocząłeś to logowanie na urządzeniu, n msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Akceptujemy tylko karty kredytowe. Kart debetowych i przedpłaconych nie można użyć do weryfikacji dostępu do treści dla dorosłych." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Tylko domyślne głośniki" @@ -24522,7 +24595,7 @@ msgstr "Tylko wybrani członkowie i role będą mogli zobaczyć ten kanał." msgid "Only show message previews for DMs with unread messages" msgstr "Pokazuj podglądy wiadomości tylko dla rozmów prywatnych z nieprzeczytanymi wiadomościami" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Tylko głośniki" @@ -24759,7 +24832,7 @@ msgstr "Otwórz szczegóły kanału „{channelName}”" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Otwórz czat" @@ -24840,7 +24913,7 @@ msgstr "Otwórz emoji w przeglądarce" msgid "Open emoji picker" msgstr "Otwórz wybór emoji" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Otwórz strumień zdarzeń" @@ -25280,6 +25353,16 @@ msgstr "Inne aktywne wątki" msgid "Other devices" msgstr "Inne urządzenia" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Inni uczestnicy mogą zobaczyć Twój adres IP." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Inni uczestnicy rozmowy P2P mogą zobaczyć Twój adres IP." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Nie udało się przenieść własności na <0>{username} w tym momen msgid "Ownership transferred" msgstr "Własność przeniesiona" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Połączenia P2P są obecnie niedostępne." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Tylko rozmowy P2P" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Przypięta grupa" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Przypięty czat grupowy" @@ -26536,7 +26635,7 @@ msgstr "Przycisk podglądu" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Podgląd kamery" @@ -27107,7 +27206,7 @@ msgstr "Przycisk „naciśnij i mów”" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Naciśnij i mów" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Skróć wiadomość i spróbuj ponownie." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Skróć temat i spróbuj ponownie." @@ -31488,7 +31587,7 @@ msgstr "Pokaż kontrolki głośności połączenia" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Pokaż skróty menu kontekstowego" @@ -31573,7 +31672,7 @@ msgstr "Pokaż mniej efektów dźwiękowych" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Pokaż ramkę fokusu w polu tekstowym czatu" @@ -31701,7 +31800,7 @@ msgstr "Pokaż wyciszone kanały" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Pokaż moją kamerę" @@ -31742,7 +31841,7 @@ msgstr "Oznaczaj nowe posty jako nieprzeczytane" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Pokaż uczestników bez wideo" @@ -32144,7 +32243,7 @@ msgstr "Ciche połączenie" msgid "Silent calls" msgstr "Ciche połączenia" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Cichy, gdy urządzenie przechwytujące nie ma własnego wejścia audio" @@ -32655,8 +32754,8 @@ msgstr "Coś poszło nie tak." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Coś poszło nie tak. Spróbuj ponownie za chwilę." @@ -32750,7 +32849,7 @@ msgstr "Dźwięki" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Źródło" @@ -32859,7 +32958,7 @@ msgstr "Mów teraz. Naciśnij „Zatrzymaj”, gdy skończysz — później moż #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Głośnik" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Rozpocznij czat grupowy" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Rozpocząć rozmowę wideo?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Rozpocząć rozmowę głosową?" @@ -33092,6 +33191,16 @@ msgstr "Data początkowa musi być wcześniejsza niż data końcowa." msgid "Start drag and drop" msgstr "Rozpocznij przeciąganie i upuszczanie" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Rozpocznij w trybie P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Rozpocząć w trybie P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Zminimalizuj przy starcie" msgid "Start over" msgstr "Zacznij od nowa" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Rozpocznij rozmowę P2P" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Zacznij wyszukiwanie od <0>{peoplePrefix} <1>{textChannelPrefix} msgid "Start speaking" msgstr "Rozpocznij odczytywanie" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Rozpocznij standardowe połączenie" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Plansza statusu" msgid "Status:" msgstr "Status:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Zostań" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Zatrzymaj" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Przestać być kanałem ogłoszeń?" @@ -33921,6 +34045,11 @@ msgstr "Przełącz na poprzednią społeczność lub wiadomości prywatne" msgid "Switch to single column" msgstr "Przełącz na widok jednokolumnowy" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Przełącz na standardowe połączenie" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Odpięto grupę" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Odpięto czat grupowy" @@ -37727,6 +37856,11 @@ msgstr "Niezweryfikowany e-mail" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Dodaj tagi, aby pomóc innym znaleźć Twoją społeczność w Odkrywaniu. Maksymalna liczba tagów: {DISCOVERY_MAX_TAGS}." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Do {maxParticipants, plural, one {# osoby} other {# osób}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Przesyłanie emoji" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Wysyłanie wideo:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Ustawienia wideo" @@ -39570,7 +39704,7 @@ msgstr "Nagrywanie wiadomości głosowych nie jest obsługiwane w tej przegląda #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Region głosowy" @@ -39582,7 +39716,7 @@ msgstr "Pasek przywracania sesji głosowej" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Ustawienia głosu" diff --git a/fluxer_app/src/features/i18n/locales/pt-BR/messages.po b/fluxer_app/src/features/i18n/locales/pt-BR/messages.po index b2e47f282..4c0822b3f 100644 --- a/fluxer_app/src/features/i18n/locales/pt-BR/messages.po +++ b/fluxer_app/src/features/i18n/locales/pt-BR/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, =0 {# mensagens bloqueadas} one {# mensagem bloqueada} other {# mensagens bloqueadas}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# canal segue} other {# canais seguem}} este canal. Convertê-lo para um canal de texto removerá essas seguidas." @@ -2720,6 +2720,11 @@ msgstr "Um emoji padrão. Você pode usá-lo em qualquer lugar no {productName}. msgid "A description is required." msgstr "É preciso adicionar uma descrição." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Uma conexão direta não pôde ser estabelecida. Vocês não podem se ouvir ou se ver." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "um conteúdo incorporado ou uma prévia de link (não um arquivo enviado msgid "A separate profile for each community" msgstr "Um perfil separado para cada comunidade" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Uma chamada padrão passa por um servidor e funciona para todos." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Os administradores pausaram os convites. Você não pode entrar agora." #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Qualquer pessoa pode solicitar uma conta, mas um administrador precisa a msgid "Anyone in my communities" msgstr "Qualquer pessoa nas minhas comunidades" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Quem não conseguir se conectar diretamente não poderá participar até que alguém mude a chamada para uma chamada padrão." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Qualquer pessoa que te encontrar pode enviar um pedido. Desative isso para escolher os grupos abaixo." @@ -5112,6 +5127,16 @@ msgstr "Áudio" msgid "Audio & video" msgstr "Áudio e vídeo" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Áudio e vídeo vão diretamente entre os participantes, para até {maxParticipants, plural, one {# pessoa} other {# pessoas}}. Todos concordam antes de entrar e podem ver o endereço IP uns dos outros." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "O áudio e o vídeo trafegam diretamente entre os participantes." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Sugestão de preenchimento automático: {label}, {selectedOptionPosition #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automático" @@ -6936,15 +6961,15 @@ msgstr "Não é possível excluir a conta" msgid "Capture" msgstr "Capturar" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Capturar áudio do app" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Capturar áudio do computador" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Capturar áudio do dispositivo" @@ -7017,7 +7042,7 @@ msgstr "ID da categoria copiado" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Nome da categoria" @@ -7554,7 +7579,7 @@ msgstr "Seções de detalhes do canal" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID do canal copiado" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "link do canal" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Link do canal copiado" @@ -7604,7 +7629,7 @@ msgstr "Gerenciamento de canais" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Canal sincronizado com a categoria principal" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "O tópico do canal é muito longo." @@ -7668,8 +7693,8 @@ msgstr "Seleção do tipo de canal" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Canal atualizado" @@ -9421,7 +9446,7 @@ msgstr "Confirmar antes de entrar em canais de voz" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Confirmar antes de iniciar chamadas" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Conversas" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Converter" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "O canal foi convertido para um canal de anúncios" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Converter este canal para um canal de texto removerá todos os canais que o seguem." @@ -9949,7 +9974,7 @@ msgstr "Copiar ID do canal" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Copiar link do canal" @@ -10379,13 +10404,18 @@ msgstr "Não foi possível concluir o login. Tente novamente." msgid "Couldn't connect to {domain}. Try again." msgstr "Não foi possível conectar a {domain}. Tente novamente." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Não foi possível conectar a {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Não foi possível conectar ao chat de voz. Tente novamente." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Não foi possível converter este canal" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Não foi possível baixar a atualização para computador" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Não foi possível duplicar este canal" @@ -10511,6 +10541,11 @@ msgstr "Não foi possível ignorar o pedido de amizade. Tente novamente." msgid "Couldn't join community" msgstr "Não foi possível entrar na comunidade" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Não foi possível entrar na chamada P2P. Tente novamente." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Não foi possível abrir o portal de cobrança" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Não foi possível fixar esta conversa" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Não foi possível executar o comando: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Não foi possível salvar as configurações do canal" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Não foi possível deixar de seguir este canal" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Não foi possível desafixar esta conversa" @@ -11374,7 +11409,7 @@ msgstr "Personalizado" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Personalizado" @@ -11938,7 +11973,7 @@ msgstr "Padrão" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Excluir \"{stickerName}\"? Não é possível desfazer." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Excluir {channelLabel}? Não poderá ser desfeito." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Áudio do dispositivo ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Áudio do dispositivo" @@ -12694,7 +12729,7 @@ msgstr "Detalhes do dispositivo" msgid "Device revoked" msgstr "Sessão do dispositivo encerrada" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Seleção de dispositivo" @@ -12758,6 +12793,11 @@ msgstr "Tamanhos e contagens de membros diferentes" msgid "Dim strikethrough text" msgstr "Escurecer texto tachado" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Conexão direta. Mídia criptografada entre os participantes." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Barra lateral de mensagens diretas" msgid "DNS TXT record" msgstr "Registro DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Não pergunte novamente, sempre concorde" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplicar" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplicar canal" @@ -13825,7 +13870,7 @@ msgstr "Editar categoria" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Editar canal" @@ -15004,7 +15049,7 @@ msgstr "Tecla Esc" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "A tecla Esc sai do modo de teclado" @@ -15047,6 +15092,11 @@ msgstr "Todas as comunidades que seguem este canal recebem uma cópia. Se você msgid "Everyone" msgstr "Todos" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Todo mundo na chamada recebe qualidade de streaming Plutonium de graça: resolução de até 4K e 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Concedeu {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Concede todas as permissões e ignora as restrições do canal. Altamente sensível." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Seleção granular" @@ -17417,7 +17467,7 @@ msgstr "Grego" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Visualização em grade" @@ -17909,7 +17959,7 @@ msgstr "Ocultar participantes" msgid "Hide password" msgstr "Ocultar senha" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Ocultar miniatura da prévia" @@ -18322,11 +18372,11 @@ msgstr "Ignorar" msgid "Ignore friend request" msgstr "Ignorar pedido de amizade" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignorar dispositivos de hardware" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignorar mídia de entrada" @@ -18357,7 +18407,7 @@ msgstr "Ignorar o pedido de amizade de {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignorar o tempo de espera do modo lento deste canal." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignorar virtual" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Dispositivo de entrada" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Nível de entrada" @@ -18865,7 +18915,7 @@ msgstr "Monitoramento de Entrada" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Perfil de entrada" @@ -19431,6 +19481,12 @@ msgstr "Variação de atraso:" msgid "Join" msgstr "Entrar" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Entrar" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Entrar em uma comunidade" msgid "Join a community to unlock stickers." msgstr "Entre em uma comunidade para desbloquear figurinhas." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Entrar em uma chamada P2P?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Método de entrada" msgid "Join on this device" msgstr "Entrar neste dispositivo" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Entrar em chamadas P2P sem pedir confirmação" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Sair de {threadName}? É privado, então você só poderá voltar se alg msgid "Leave blank for a random username" msgstr "Deixe em branco para um nome de usuário aleatório" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Sair da chamada" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Permitir que o CSS personalizado revele regiões transparentes da janela após reiniciar o aplicativo para computador." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Permite que outras comunidades sigam este canal e recebam cópias do que você publicar." @@ -20729,6 +20797,11 @@ msgstr "Luisiana" msgid "Low" msgstr "Baixa" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Latência menor." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Espelhar câmera" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Minha figurinha incrível" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Minha categoria" @@ -24444,7 +24517,7 @@ msgstr "Continue somente se você iniciou esta entrada em um dispositivo que voc msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Apenas cartões de crédito são aceitos. Cartões de débito e pré-pagos não podem ser usados para a verificação de conteúdo adulto." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Apenas alto-falantes padrão" @@ -24522,7 +24595,7 @@ msgstr "Somente membros e cargos selecionados poderão ver este canal." msgid "Only show message previews for DMs with unread messages" msgstr "Mostrar prévias apenas das conversas por mensagem direta com mensagens não lidas" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Apenas alto-falantes" @@ -24759,7 +24832,7 @@ msgstr "Abrir detalhes do canal {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Abrir chat" @@ -24840,7 +24913,7 @@ msgstr "Abrir emoji no navegador" msgid "Open emoji picker" msgstr "Abrir seletor de emojis" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Abrir coletor de eventos" @@ -25280,6 +25353,16 @@ msgstr "Outras threads ativas" msgid "Other devices" msgstr "Outros dispositivos" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Outros participantes podem ver seu endereço IP." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Outros participantes em uma chamada P2P podem ver seu endereço IP." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Não foi possível transferir a propriedade para <0>{username} neste msgid "Ownership transferred" msgstr "Propriedade transferida" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Chamadas P2P não estão disponíveis no momento." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Chamadas P2P apenas" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Grupo fixado" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Conversa em grupo fixada" @@ -26536,7 +26635,7 @@ msgstr "Botão de pré-visualização" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Prévia da câmera" @@ -27107,7 +27206,7 @@ msgstr "Tecla para apertar e falar" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Apertar para falar" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Encurte a mensagem e tente novamente." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Encurte o tópico e tente novamente." @@ -31488,7 +31587,7 @@ msgstr "Mostrar controles de volume de conexão" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Mostrar atalhos do menu de contexto" @@ -31573,7 +31672,7 @@ msgstr "Mostrar menos efeitos sonoros" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Mostrar anel de foco na caixa de texto do chat" @@ -31701,7 +31800,7 @@ msgstr "Mostrar canais silenciados" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Mostrar minha câmera" @@ -31742,7 +31841,7 @@ msgstr "Marcar novas postagens como não lidas" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Mostrar participantes sem vídeo" @@ -32144,7 +32243,7 @@ msgstr "Chamada silenciosa" msgid "Silent calls" msgstr "Chamadas silenciosas" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Silencioso quando o dispositivo de captura não tem entrada de áudio própria" @@ -32655,8 +32754,8 @@ msgstr "Algo deu errado." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Algo deu errado. Tente novamente em alguns instantes." @@ -32750,7 +32849,7 @@ msgstr "Sons" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Original" @@ -32859,7 +32958,7 @@ msgstr "Fale agora. Pressione Parar quando terminar — você pode cortar depois #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Alto-falante" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Iniciar uma conversa em grupo" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Iniciar uma chamada de vídeo?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Iniciar uma chamada de voz?" @@ -33092,6 +33191,16 @@ msgstr "A data de início deve ser anterior à data de término." msgid "Start drag and drop" msgstr "Iniciar arrastar e soltar" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Iniciar em modo P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Iniciar no modo P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Iniciar minimizado" msgid "Start over" msgstr "Começar de novo" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Iniciar chamada P2P" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Comece as pesquisas com <0>{peoplePrefix} <1>{textChannelPrefix} msgid "Start speaking" msgstr "Iniciar leitura em voz alta" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Iniciar chamada padrão" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Quadro de status" msgid "Status:" msgstr "Status:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Continuar" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Parar" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Parar de ser um canal de anúncios?" @@ -33921,6 +34045,11 @@ msgstr "Mudar para a comunidade anterior ou mensagens diretas" msgid "Switch to single column" msgstr "Mudar para coluna única" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Mudar para chamada padrão" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Grupo desafixado" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Conversa em grupo desafixada" @@ -37727,6 +37856,11 @@ msgstr "E-mail não verificado" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Use até {DISCOVERY_MAX_TAGS} tags para ajudar as pessoas a encontrar sua comunidade. Elas aparecem nas pesquisas em Descobrir." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Até {maxParticipants, plural, one {# pessoa} other {# pessoas}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Enviando emojis" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Envio de vídeo:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Configurações de vídeo" @@ -39570,7 +39704,7 @@ msgstr "A gravação de voz não é compatível com este navegador." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Região de voz" @@ -39582,7 +39716,7 @@ msgstr "Barra de notificação de restauração de sessão de voz" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Configurações de voz" diff --git a/fluxer_app/src/features/i18n/locales/ro/messages.po b/fluxer_app/src/features/i18n/locales/ro/messages.po index 965b3f96e..209de15a4 100644 --- a/fluxer_app/src/features/i18n/locales/ro/messages.po +++ b/fluxer_app/src/features/i18n/locales/ro/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# mesaj blocat} few {# mesaje blocate} other {# de mesaje blocate}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# canal urmărește} other {# canale urmăresc}} acest canal. Convertirea lui într-un canal de text va elimina acele urmăriri." @@ -2720,6 +2720,11 @@ msgstr "O emoji implicită. O poți folosi oriunde pe {productName}." msgid "A description is required." msgstr "Este necesară o descriere." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Nu s-a putut stabili o conexiune directă. Nu vă puteți auzi sau vedea." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "o încorporare complexă sau o previzualizare a linkului (nu un fișier msgid "A separate profile for each community" msgstr "Un profil separat pentru fiecare comunitate" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Un apel standard trece printr-un server și funcționează pentru toată lumea." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Administratorii au întrerupt invitațiile, așa că nu te poți alătur #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Oricine poate solicita un cont, dar un administrator trebuie să-l aprob msgid "Anyone in my communities" msgstr "Oricine din comunitățile mele" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Oricine nu se poate conecta direct nu poate participa până când cineva nu comută apelul la un apel standard." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Oricine te găsește îți poate trimite o cerere. Dezactivează această opțiune pentru a alege grupuri mai jos." @@ -5112,6 +5127,16 @@ msgstr "Audio" msgid "Audio & video" msgstr "Audio și video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Audio și video circulă direct între participanți, până la {maxParticipants, plural, one {# persoană} other {# persoane}}. Toți își dau acordul înainte de a intra și își pot vedea adresele IP." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Audio și video circulă direct între participanți." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Sugestie de completare automată: {label}, {selectedOptionPosition} din #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automat" @@ -6936,15 +6961,15 @@ msgstr "Nu se poate șterge contul" msgid "Capture" msgstr "Captură" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Capturează sunetul aplicației" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Capturează sunetul de pe desktop" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Capturează sunetul dispozitivului" @@ -7017,7 +7042,7 @@ msgstr "ID categorie copiat" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Numele categoriei" @@ -7554,7 +7579,7 @@ msgstr "Secțiuni detalii canal" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID canal copiat" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "link canal" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Linkul canalului a fost copiat" @@ -7604,7 +7629,7 @@ msgstr "Gestionare canale" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Canal sincronizat cu categoria părinte" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Subiectul canalului este prea lung." @@ -7668,8 +7693,8 @@ msgstr "Selectare tip canal" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Canal actualizat" @@ -9421,7 +9446,7 @@ msgstr "Confirmă înainte de a te alătura canalelor vocale" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Confirmă înainte de a iniția apeluri" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Conversații" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Convertește" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "A transformat canalul într-un canal de anunțuri" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Convertirea acestui canal într-un canal text va elimina fiecare canal care îl urmează." @@ -9949,7 +9974,7 @@ msgstr "Copiază ID-ul canalului" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Copiază linkul canalului" @@ -10379,13 +10404,18 @@ msgstr "Nu s-a putut finaliza autentificarea. Încearcă din nou." msgid "Couldn't connect to {domain}. Try again." msgstr "Nu am putut să mă conectez la {domain}. Încearcă din nou." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Nu am putut să mă conectez la {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Conectarea la voce nu a reușit. Te rugăm să încerci din nou." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Nu am putut converti acest canal" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Nu s-a putut descărca actualizarea pentru desktop" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Nu s-a putut duplica acest canal" @@ -10511,6 +10541,11 @@ msgstr "Nu s-a putut ignora cererea de prietenie. Încearcă din nou." msgid "Couldn't join community" msgstr "Nu te-ai putut alătura comunității" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Nu am putut să mă alătur apelului P2P. Încearcă din nou." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Nu s-a putut deschide portalul de facturare" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Nu s-a putut fixa această conversație" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Nu s-a putut rula comanda: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Nu am putut salva setările canalului" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Nu s-a putut renunța la urmărirea acestui canal" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Nu s-a putut anula fixarea acestei conversații" @@ -11374,7 +11409,7 @@ msgstr "Personalizat" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Personalizat" @@ -11938,7 +11973,7 @@ msgstr "Implicit" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Ștergi „{stickerName}”? Această acțiune nu poate fi anulată." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Ștergi {channelLabel}? Această acțiune nu poate fi anulată." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Audio dispozitiv ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Doar audio dispozitiv" @@ -12694,7 +12729,7 @@ msgstr "Detalii dispozitiv" msgid "Device revoked" msgstr "Dispozitiv revocat" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Selectare dispozitiv" @@ -12758,6 +12793,11 @@ msgstr "Dimensiuni și numere de membri diferite" msgid "Dim strikethrough text" msgstr "Estompează textul tăiat" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Conexiune directă. Media este criptată între participanți." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Bară laterală DM" msgid "DNS TXT record" msgstr "Înregistrare DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Nu mai întreba, acceptă întotdeauna" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplicare" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplică canalul" @@ -13825,7 +13870,7 @@ msgstr "Editează categoria" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Editează canalul" @@ -15004,7 +15049,7 @@ msgstr "Tasta Esc" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Tasta Esc iese din modul tastatură" @@ -15047,6 +15092,11 @@ msgstr "Fiecare comunitate care urmărește acest canal va primi o copie. Dacă msgid "Everyone" msgstr "Toată lumea" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Toți cei din apel beneficiază gratuit de calitatea fluxului Plutonium: rezoluție sursă de până la 4K și 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Acordat {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Acordă toate permisiunile și ocolește restricțiile canalului. Foarte sensibil." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Selecție granulară" @@ -17417,7 +17467,7 @@ msgstr "Greacă" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Vizualizare grilă" @@ -17909,7 +17959,7 @@ msgstr "Ascunde participanții" msgid "Hide password" msgstr "Ascunde parola" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Ascunde miniatura de previzualizare" @@ -18322,11 +18372,11 @@ msgstr "Ignoră" msgid "Ignore friend request" msgstr "Ignoră cererea de prietenie" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignoră dispozitivele hardware" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignoră media de intrare" @@ -18357,7 +18407,7 @@ msgstr "Ignori cererea de prietenie de la {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignoră pauza impusă de modul lent al acestui canal." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignoră dispozitivele virtuale" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Dispozitiv de intrare" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Nivel de intrare" @@ -18865,7 +18915,7 @@ msgstr "Monitorizare intrare" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Profil de intrare" @@ -19431,6 +19481,12 @@ msgstr "Jitter:" msgid "Join" msgstr "Alăturare" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Participă" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Alătură-te unei comunități" msgid "Join a community to unlock stickers." msgstr "Alătură-te unei comunități pentru a debloca stickere." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Participi la un apel P2P?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Metodă de alăturare" msgid "Join on this device" msgstr "Participă de pe acest dispozitiv" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Intră în apeluri P2P fără confirmare" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Părăsești {threadName}? Este privat, așa că te poți întoarce doar msgid "Leave blank for a random username" msgstr "Lasă necompletat pentru un nume de utilizator aleatoriu" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Părăsește apelul" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Permite CSS-ului personalizat să afișeze zone transparente ale ferestrei după repornirea aplicației desktop." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Permite altor comunități să urmărească acest canal și să primească copii ale celor publicate." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Scăzut" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Latență mai mică." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Oglindește camera" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Stickerul meu minunat" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Categoria mea" @@ -24444,7 +24517,7 @@ msgstr "Continuă doar dacă ai început această autentificare pe un dispozitiv msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Sunt acceptate doar cardurile de credit. Cardurile de debit și preplătite nu pot fi utilizate pentru verificarea conținutului matur." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Doar difuzoare implicite" @@ -24522,7 +24595,7 @@ msgstr "Doar membrii și rolurile selectate vor putea vedea acest canal." msgid "Only show message previews for DMs with unread messages" msgstr "Afișează previzualizările mesajelor doar pentru DM-urile cu mesaje necitite" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Doar difuzoare" @@ -24759,7 +24832,7 @@ msgstr "Deschide detaliile canalului {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Deschide chatul" @@ -24840,7 +24913,7 @@ msgstr "Deschide emoji în browser" msgid "Open emoji picker" msgstr "Deschide selectorul de emoji" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Deschide fluxul de evenimente" @@ -25280,6 +25353,16 @@ msgstr "Alte fire active" msgid "Other devices" msgstr "Alte dispozitive" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Alți participanți vă pot vedea adresa IP." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Ceilalți participanți la un apel P2P îți pot vedea adresa IP." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Dreptul de proprietate nu a putut fi transferat către <0>{username} msgid "Ownership transferred" msgstr "Proprietate transferată" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Apelurile peer-to-peer nu sunt disponibile momentan." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Apeluri doar peer-to-peer" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Grup fixat" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "DM de grup fixat" @@ -26536,7 +26635,7 @@ msgstr "Buton de previzualizare" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Previzualizare cameră" @@ -27107,7 +27206,7 @@ msgstr "Tastă push-to-talk" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Push-to-talk" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Scurtează mesajul și încearcă din nou." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Scurtează subiectul și încearcă din nou." @@ -31488,7 +31587,7 @@ msgstr "Afișează controalele volumului conexiunii" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Afișează comenzile rapide pentru meniul contextual" @@ -31573,7 +31672,7 @@ msgstr "Afișează mai puține efecte sonore" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Afișează inelul de focalizare pe zona de text a conversației" @@ -31701,7 +31800,7 @@ msgstr "Afișează canalele puse pe silențios" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Afișează-mi propria cameră" @@ -31742,7 +31841,7 @@ msgstr "Afișează postările noi ca necitite" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Afișează participanții fără video" @@ -32144,7 +32243,7 @@ msgstr "Apel silențios" msgid "Silent calls" msgstr "Apeluri silențioase" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Silenzios când dispozitivul de captură nu are propria intrare audio" @@ -32655,8 +32754,8 @@ msgstr "Ceva nu a mers bine." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Ceva nu a mers bine. Te rugăm să încerci din nou mai târziu." @@ -32750,7 +32849,7 @@ msgstr "Sunete" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Sursă" @@ -32859,7 +32958,7 @@ msgstr "Vorbește acum. Apasă Oprește când ai terminat — poți decupa ulter #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Difuzor" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Începe un DM de grup" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Începi un apel video?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Începi un apel vocal?" @@ -33092,6 +33191,16 @@ msgstr "Data de început trebuie să fie anterioară datei de sfârșit." msgid "Start drag and drop" msgstr "Începe glisarea și fixarea" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Pornește în modul P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Începe în modul P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Pornește minimizat" msgid "Start over" msgstr "Ia-o de la capăt" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Pornește apel P2P" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Începe căutările cu <0>{peoplePrefix} <1>{textChannelPrefix} msgid "Start speaking" msgstr "Începe citirea" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Pornește apel standard" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Panou de stare" msgid "Status:" msgstr "Stare:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Păstrează" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Oprește" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Opriți transformarea în canal de anunțuri?" @@ -33921,6 +34045,11 @@ msgstr "Comută la comunitatea anterioară sau la DM-uri" msgid "Switch to single column" msgstr "Treci la o singură coloană" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Comută la apel standard" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Fixarea grupului a fost anulată" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Fixarea DM-ului de grup a fost anulată" @@ -37727,6 +37856,11 @@ msgstr "E-mail neverificat" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Până la {DISCOVERY_MAX_TAGS} etichete îi ajută pe oameni să-ți găsească comunitatea. Acestea apar în căutarea Discovery." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Până la {maxParticipants, plural, one {# persoană} other {# persoane}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Se încarcă emoji" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Trimitere video:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Setări video" @@ -39570,7 +39704,7 @@ msgstr "Înregistrarea vocală nu este acceptată în acest browser." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Regiune vocală" @@ -39582,7 +39716,7 @@ msgstr "Bara de notificare pentru restaurarea sesiunii vocale" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Setări voce" diff --git a/fluxer_app/src/features/i18n/locales/ru/messages.po b/fluxer_app/src/features/i18n/locales/ru/messages.po index 3c4ec9780..c69bfa479 100644 --- a/fluxer_app/src/features/i18n/locales/ru/messages.po +++ b/fluxer_app/src/features/i18n/locales/ru/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# заблокированное сообщение} few {# заблокированных сообщения} many {# заблокированных сообщений} other {# заблокированных сообщений}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# канал подписан} few {# канала подписаны} many {# каналов подписаны} other {# канала подписаны}} на этот канал. Преобразование в текстовый канал отменит эти подписки." @@ -2720,6 +2720,11 @@ msgstr "Стандартный эмодзи. Вы можете использо msgid "A description is required." msgstr "Требуется описание." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Не удалось установить прямое соединение. Вы не можете слышать или видеть друг друга." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "расширенный встраиваемый элемент или п msgid "A separate profile for each community" msgstr "Отдельный профиль для каждого сообщества" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Обычный звонок проходит через сервер и работает у всех." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Администраторы приостановили приглаше #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Любой может запросить аккаунт, но админ msgid "Anyone in my communities" msgstr "Любой из моих сообществ" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Если кто-то не сможет подключиться напрямую, он не сможет участвовать, пока кто-нибудь не переключит звонок на стандартный." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Любой, кто вас найдет, сможет отправить запрос. Отключите эту опцию, чтобы выбрать группы ниже." @@ -5112,6 +5127,16 @@ msgstr "Аудио" msgid "Audio & video" msgstr "Аудио и видео" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Аудио и видео передаются напрямую между участниками, максимум {maxParticipants, plural, one {# человек} other {# человека}}. Все соглашаются перед подключением и могут видеть IP-адрес друг друга." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Аудио и видео передаются напрямую между участниками." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Подсказка: {label}, {selectedOptionPosition} из {resultCount} #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Автоматически" @@ -6936,15 +6961,15 @@ msgstr "Невозможно удалить аккаунт" msgid "Capture" msgstr "Захват" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Захватывать звук приложения" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Захватывать звук рабочего стола" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Захватывать звук устройства" @@ -7017,7 +7042,7 @@ msgstr "ID категории скопирован" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Название категории" @@ -7554,7 +7579,7 @@ msgstr "Разделы сведений о канале" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "ID канала скопирован" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "ссылка на канал" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Ссылка на канал скопирована" @@ -7604,7 +7629,7 @@ msgstr "Управление каналами" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Канал синхронизирован с родительской категорией" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Тема канала слишком длинная." @@ -7668,8 +7693,8 @@ msgstr "Выбор типа канала" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Канал обновлен" @@ -9421,7 +9446,7 @@ msgstr "Запрашивать подтверждение перед входо #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Запрашивать подтверждение перед звонком" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Беседы" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Преобразовать" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Канал был преобразован в канал объявлений" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "При преобразовании этого канала в текстовый все следующие за ним каналы будут удалены." @@ -9949,7 +9974,7 @@ msgstr "Скопировать ID канала" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Скопировать ссылку на канал" @@ -10379,13 +10404,18 @@ msgstr "Не удалось завершить вход. Повторите по msgid "Couldn't connect to {domain}. Try again." msgstr "Не удалось подключиться к {domain}. Повторите попытку." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Не удалось подключиться к {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Не удалось подключиться к голосовому чату. Попробуйте ещё раз." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Не удалось преобразовать этот канал" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Не удалось загрузить обновление для ПК" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Не удалось дублировать этот канал" @@ -10511,6 +10541,11 @@ msgstr "Не удалось проигнорировать запрос на д msgid "Couldn't join community" msgstr "Не удалось присоединиться к сообществу" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Не удалось присоединиться к P2P-вызову. Попробуйте снова." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Не удалось открыть портал оплаты" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Не удалось закрепить этот чат" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Не удалось выполнить команду: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Не удалось сохранить настройки канала" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Не удалось отписаться от этого канала" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Не удалось открепить этот чат" @@ -11374,7 +11409,7 @@ msgstr "Другой источник" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Пользовательский" @@ -11938,7 +11973,7 @@ msgstr "По умолчанию" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Удалить «{stickerName}»? Это действие нельзя #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Удалить {channelLabel}? Это действие нельзя отменить." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Аудио устройства ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Аудио только с устройства" @@ -12694,7 +12729,7 @@ msgstr "Информация об устройстве" msgid "Device revoked" msgstr "Устройство отозвано" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Выбор устройства" @@ -12758,6 +12793,11 @@ msgstr "Разные размеры и количество участников msgid "Dim strikethrough text" msgstr "Приглушить зачеркнутый текст" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Прямое соединение. Медиа зашифровано между участниками." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Боковая панель чатов" msgid "DNS TXT record" msgstr "TXT-запись DNS" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Больше не спрашивать, всегда соглашаться" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Дублировать" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Дублировать канал" @@ -13825,7 +13870,7 @@ msgstr "Изменить категорию" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Изменить канал" @@ -15004,7 +15049,7 @@ msgstr "Клавиша Esc" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Клавиша Escape отключает режим клавиатуры" @@ -15047,6 +15092,11 @@ msgstr "Каждое сообщество, подписанное на этот msgid "Everyone" msgstr "Все" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Все участники звонка бесплатно получают качество потока Plutonium: до 4K исходного разрешения и 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Предоставлены разрешения: {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Предоставляет все разрешения и обходит ограничения канала. Крайне рискованно." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Детальный выбор" @@ -17417,7 +17467,7 @@ msgstr "Греческий" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Сетка" @@ -17909,7 +17959,7 @@ msgstr "Скрыть участников" msgid "Hide password" msgstr "Скрыть пароль" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Скрыть миниатюру предпросмотра" @@ -18322,11 +18372,11 @@ msgstr "Игнорировать" msgid "Ignore friend request" msgstr "Игнорировать запрос в друзья" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Игнорировать аппаратные устройства" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Игнорировать входные медиапотоки" @@ -18357,7 +18407,7 @@ msgstr "Игнорировать запрос в друзья от {displayName} msgid "Ignore this channel's slowmode cooldown." msgstr "Игнорировать медленный режим в этом канале." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Игнорировать виртуальные устройства" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Устройство ввода" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Уровень входного сигнала" @@ -18865,7 +18915,7 @@ msgstr "Мониторинг ввода" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Профиль ввода" @@ -19431,6 +19481,12 @@ msgstr "Джиттер:" msgid "Join" msgstr "Присоединиться" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Присоединиться" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Присоединиться к сообществу" msgid "Join a community to unlock stickers." msgstr "Присоединитесь к сообществу, чтобы разблокировать стикеры." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Начать звонок P2P?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Способ вступления" msgid "Join on this device" msgstr "Присоединиться с этого устройства" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Присоединяться к P2P-звонкам без подтверждения" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Выйти из чата {threadName}? Он приватный, поэт msgid "Leave blank for a random username" msgstr "Оставьте пустым для случайного имени пользователя" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Выйти из звонка" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Позволяет пользовательскому CSS отображать прозрачные области окна после перезапуска приложения на компьютере." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Позволяет другим сообществам подписаться на этот канал и получать копии ваших публикаций." @@ -20729,6 +20797,11 @@ msgstr "Луизиана" msgid "Low" msgstr "Низкий" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Меньше задержка." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Мичиган" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Миннесота" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Зеркальное отображение камеры" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Мой классный стикер" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Моя категория" @@ -24444,7 +24517,7 @@ msgstr "Продолжайте, только если вы начали этот msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Принимаются только кредитные карты. Дебетовые и предоплаченные карты нельзя использовать для проверки доступа к контенту для взрослых." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Только динамики по умолчанию" @@ -24522,7 +24595,7 @@ msgstr "Этот канал смогут просматривать только msgid "Only show message previews for DMs with unread messages" msgstr "Показывать предпросмотр сообщений только для личных чатов с непрочитанными сообщениями" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Только динамики" @@ -24759,7 +24832,7 @@ msgstr "Открыть информацию о канале «{channelName}»" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Открыть чат" @@ -24840,7 +24913,7 @@ msgstr "Открыть эмодзи в браузере" msgid "Open emoji picker" msgstr "Открыть выбор эмодзи" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Открыть журнал событий" @@ -25280,6 +25353,16 @@ msgstr "Другие активные темы" msgid "Other devices" msgstr "Другие устройства" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Другие участники могут видеть ваш IP-адрес." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "IP-адрес других участников P2P-звонка будет виден." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Сейчас не удалось передать права владе msgid "Ownership transferred" msgstr "Права владельца переданы" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "P2P-вызовы недоступны прямо сейчас." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Только P2P-звонки" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Закрепленная группа" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Закреплённый групповой чат" @@ -26536,7 +26635,7 @@ msgstr "Кнопка предпросмотра" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Предпросмотр камеры" @@ -27107,7 +27206,7 @@ msgstr "Клавиша «Нажми и говори»" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Нажми и говори" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Сократите сообщение и попробуйте снова." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Сократите тему и попробуйте снова." @@ -31488,7 +31587,7 @@ msgstr "Показывать элементы управления громко #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Показывать сочетания клавиш в контекстном меню" @@ -31573,7 +31672,7 @@ msgstr "Показать меньше звуковых эффектов" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Показывать рамку фокуса в поле ввода чата" @@ -31701,7 +31800,7 @@ msgstr "Показывать каналы без уведомлений" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Показывать мою камеру" @@ -31742,7 +31841,7 @@ msgstr "Помечать новые сообщения как непрочита #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Показывать участников без видео" @@ -32144,7 +32243,7 @@ msgstr "Тихий звонок" msgid "Silent calls" msgstr "Тихие звонки" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Без звука, если у устройства захвата нет собственного аудиовхода" @@ -32655,8 +32754,8 @@ msgstr "Что-то пошло не так." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Что-то пошло не так. Повторите попытку через минуту." @@ -32750,7 +32849,7 @@ msgstr "Звуки" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Исходное" @@ -32859,7 +32958,7 @@ msgstr "Говорите. Нажмите «Остановить», когда з #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Динамик" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Создать групповой чат" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Начать видеозвонок?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Начать голосовой звонок?" @@ -33092,6 +33191,16 @@ msgstr "Дата начала должна быть раньше даты око msgid "Start drag and drop" msgstr "Начать перетаскивание" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Начать в режиме P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Начать в режиме P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Начинать свернутым" msgid "Start over" msgstr "Начать сначала" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Начать P2P-звонок" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Начните поиск с <0>{peoplePrefix} <1>{textChannelPrefix msgid "Start speaking" msgstr "Начать озвучивание" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Начать обычный звонок" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Панель состояния" msgid "Status:" msgstr "Статус:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Остаться" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Стоп" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Прекратить быть каналом объявлений?" @@ -33921,6 +34045,11 @@ msgstr "Переключиться на предыдущее сообществ msgid "Switch to single column" msgstr "Переключиться на одну колонку" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Переключиться на обычный звонок" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Группа откреплена" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Групповой чат откреплен" @@ -37727,6 +37856,11 @@ msgstr "Эл. почта не подтверждена" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "До {DISCOVERY_MAX_TAGS} тегов помогут людям найти ваше сообщество. Они отображаются в поиске по каталогу." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "До {maxParticipants, plural, one {# человека} few {# человека} many {# человек} other {# человека}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Загрузка эмодзи" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Видео (отправка):" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Настройки видео" @@ -39570,7 +39704,7 @@ msgstr "Запись голосовых сообщений не поддержи #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Регион для голосовых чатов" @@ -39582,7 +39716,7 @@ msgstr "Баннер восстановления голосовой сесси #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Настройки голоса" diff --git a/fluxer_app/src/features/i18n/locales/sv-SE/messages.po b/fluxer_app/src/features/i18n/locales/sv-SE/messages.po index f1ddb728a..33e261895 100644 --- a/fluxer_app/src/features/i18n/locales/sv-SE/messages.po +++ b/fluxer_app/src/features/i18n/locales/sv-SE/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# blockerat meddelande} other {# blockerade meddelanden}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# kanal följer} other {# kanaler följer}} den här kanalen. Om du konverterar den till en textkanal tas dessa följare bort." @@ -2720,6 +2720,11 @@ msgstr "En standardemoji. Du kan använda den var som helst i {productName}." msgid "A description is required." msgstr "En beskrivning krävs." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "En direkt anslutning kunde inte upprättas. Ni kan inte höra eller se varandra." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "en inbäddning eller länkförhandsvisning (inte en uppladdad fil)" msgid "A separate profile for each community" msgstr "En separat profil för varje community" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Ett vanligt samtal går via en server och fungerar för alla." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Administratörer pausade inbjudningar – du kan inte gå med just nu." #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Vem som helst kan begära ett konto, men en administratör måste godkä msgid "Anyone in my communities" msgstr "Alla i mina communities" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Alla som inte kan ansluta direkt kan inte delta förrän någon växlar samtalet till ett vanligt samtal." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Alla som hittar dig kan skicka en begäran. Stäng av detta för att välja grupper nedan." @@ -5112,6 +5127,16 @@ msgstr "Ljud" msgid "Audio & video" msgstr "Ljud och video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Ljud och video går direkt mellan deltagarna, upp till {maxParticipants, plural, one {# person} other {# personer}}. Alla godkänner innan de ansluter och kan se varandras IP-adresser." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Ljud och video skickas direkt mellan deltagarna." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Föreslagen komplettering: {label}, {selectedOptionPosition} av {resultC #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Automatisk" @@ -6936,15 +6961,15 @@ msgstr "Kan inte radera konto" msgid "Capture" msgstr "Spela in" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Spela in app-ljud" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Spela in datorljud" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Spela in enhetsljud" @@ -7017,7 +7042,7 @@ msgstr "Kategori-ID kopierat" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Kategorinamn" @@ -7554,7 +7579,7 @@ msgstr "Avsnitt med kanalinformation" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Kanal-ID kopierat" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "kanallänk" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Kanallänk kopierad" @@ -7604,7 +7629,7 @@ msgstr "Kanalhantering" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Kanalen är synkroniserad med den överordnade kategorin" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Kanalämnet är för långt." @@ -7668,8 +7693,8 @@ msgstr "Välj kanaltyp" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Kanalen uppdaterades" @@ -9421,7 +9446,7 @@ msgstr "Bekräfta innan du går med i röstkanaler" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Bekräfta innan samtal startas" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Konversationer" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Konvertera" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Konverterade kanalen till en meddelandekanal" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Att konvertera den här kanalen till en textkanal tar bort alla kanaler som följer den." @@ -9949,7 +9974,7 @@ msgstr "Kopiera kanal-ID" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Kopiera kanallänk" @@ -10379,13 +10404,18 @@ msgstr "Kunde inte slutföra inloggningen. Försök igen." msgid "Couldn't connect to {domain}. Try again." msgstr "Kunde inte ansluta till {domain}. Försök igen." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Kunde inte ansluta till {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Kunde inte ansluta till röstchatten. Försök igen." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Kunde inte konvertera den här kanalen" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Kunde inte ladda ner uppdateringen för datorappen" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Kunde inte duplicera kanalen" @@ -10511,6 +10541,11 @@ msgstr "Kunde inte ignorera vänförfrågan. Försök igen." msgid "Couldn't join community" msgstr "Kunde inte gå med i communityn" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Kunde inte ansluta till P2P-samtalet. Försök igen." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Kunde inte öppna faktureringsportalen" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Kunde inte fästa den här konversationen" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Kunde inte köra kommandot: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Kunde inte spara kanalinställningar" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Kunde inte sluta följa den här kanalen" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Kunde inte lossa den här konversationen" @@ -11374,7 +11409,7 @@ msgstr "Anpassad källa" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Anpassad" @@ -11938,7 +11973,7 @@ msgstr "Standard" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Ta bort \"{stickerName}\"? Detta kan inte ångras." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Ta bort {channelLabel}? Detta kan inte ångras." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Enhetsljud ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Endast enhetsljud" @@ -12694,7 +12729,7 @@ msgstr "Enhetsinformation" msgid "Device revoked" msgstr "Enhetens åtkomst har återkallats" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Enhetsval" @@ -12758,6 +12793,11 @@ msgstr "Olika storlekar och antal medlemmar" msgid "Dim strikethrough text" msgstr "Tona ned genomstruken text" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Direkt anslutning. Media krypteras mellan deltagarna." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "DM-sidofält" msgid "DNS TXT record" msgstr "TXT-post i DNS" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Fråga inte igen, godkänn alltid" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Duplicera" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Duplicera kanal" @@ -13825,7 +13870,7 @@ msgstr "Redigera kategori" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Redigera kanal" @@ -15004,7 +15049,7 @@ msgstr "Escape-tangenten" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Esc-tangenten avslutar tangentbordsläget" @@ -15047,6 +15092,11 @@ msgstr "Alla communities som följer den här kanalen får en kopia. Om du redig msgid "Everyone" msgstr "Alla" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Alla i samtalet får Plutonium-strömning av hög kvalitet gratis: upp till 4K källupplösning och 60 bilder/sekund." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Beviljade {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Ger alla behörigheter och kringgår kanalbegränsningar. Mycket känsligt." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Detaljerat urval" @@ -17417,7 +17467,7 @@ msgstr "Grekiska" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Rutnätsvy" @@ -17909,7 +17959,7 @@ msgstr "Dölj deltagare" msgid "Hide password" msgstr "Dölj lösenord" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Dölj förhandsgranskningens miniatyrbild" @@ -18322,11 +18372,11 @@ msgstr "Ignorera" msgid "Ignore friend request" msgstr "Ignorera vänförfrågan" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ignorera maskinvaruenheter" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ignorera inmatningsmedia" @@ -18357,7 +18407,7 @@ msgstr "Ignorera vänförfrågan från {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Ignorera väntetiden för långsamt läge i den här kanalen." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ignorera virtuella" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Indataenhet" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Ingångsnivå" @@ -18865,7 +18915,7 @@ msgstr "Inmatningsövervakning" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Ingångsprofil" @@ -19431,6 +19481,12 @@ msgstr "Jitter:" msgid "Join" msgstr "Gå med" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Gå med" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Gå med i en community" msgid "Join a community to unlock stickers." msgstr "Gå med i en community för att låsa upp klistermärken." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Starta ett P2P-samtal?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Anslutningsmetod" msgid "Join on this device" msgstr "Gå med på den här enheten" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Gå med i P2P-samtal utan att fråga" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Lämna {threadName}? Det är privat, så du kan bara komma tillbaka om n msgid "Leave blank for a random username" msgstr "Lämna tomt för ett slumpmässigt användarnamn" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Lämna samtal" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Låter anpassad CSS visa transparenta fönsterregioner efter omstart av datorappen." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Låt andra communities följa den här kanalen och få kopior av det du publicerar." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Låg" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Lägre fördröjning." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Spegelvänd kamera" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Mitt snygga klistermärke" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Min kategori" @@ -24444,7 +24517,7 @@ msgstr "Fortsätt bara om du påbörjade den här inloggningen på en enhet du k msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Endast kreditkort accepteras. Bankkort och förbetalda kort kan inte användas för kontrollen för vuxet innehåll." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Endast standardhögtalare" @@ -24522,7 +24595,7 @@ msgstr "Endast utvalda medlemmar och roller kommer att kunna se den här kanalen msgid "Only show message previews for DMs with unread messages" msgstr "Visa bara meddelandeförhandsvisningar för DM-konversationer med olästa meddelanden" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Endast högtalare" @@ -24759,7 +24832,7 @@ msgstr "Öppna kanalinformation för {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Öppna chatt" @@ -24840,7 +24913,7 @@ msgstr "Öppna emoji i webbläsaren" msgid "Open emoji picker" msgstr "Öppna emojiväljaren" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Öppna händelsehanterare" @@ -25280,6 +25353,16 @@ msgstr "Andra aktiva trådar" msgid "Other devices" msgstr "Andra enheter" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Andra deltagare kan se din IP-adress." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Andra deltagare i ett P2P-samtal kan se din IP-adress." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Ägarskapet kunde inte överföras till <0>{username} just nu." msgid "Ownership transferred" msgstr "Ägarskap överfört" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "P2P-samtal är inte tillgängliga just nu." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Endast P2P-samtal" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Fäst grupp" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Fäst grupp-DM" @@ -26536,7 +26635,7 @@ msgstr "Förhandsvisningsknapp" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Förhandsgranska kamera" @@ -27107,7 +27206,7 @@ msgstr "Tangent för tryck för att prata" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Tryck för att prata" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Korta ner meddelandet och försök igen." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Korta ner ämnet och försök igen." @@ -31488,7 +31587,7 @@ msgstr "Visa volymkontroller för anslutning" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Visa genvägar för snabbmenyn" @@ -31573,7 +31672,7 @@ msgstr "Visa färre ljudeffekter" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Visa fokusring på chattens textruta" @@ -31701,7 +31800,7 @@ msgstr "Visa tystade kanaler" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Visa min egen kamera" @@ -31742,7 +31841,7 @@ msgstr "Visa nya inlägg som olästa" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Visa deltagare utan video" @@ -32144,7 +32243,7 @@ msgstr "Tyst samtal" msgid "Silent calls" msgstr "Tysta samtal" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Tyst när enheten som används för inspelning inte har någon egen ljudingång" @@ -32655,8 +32754,8 @@ msgstr "Något gick fel." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Något gick fel. Försök igen om en liten stund." @@ -32750,7 +32849,7 @@ msgstr "Ljud" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Källa" @@ -32859,7 +32958,7 @@ msgstr "Tala nu. Tryck på Stopp när du är klar – du kan trimma efteråt." #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Högtalare" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Starta en grupp-DM-konversation" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Starta ett videosamtal?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Starta ett röstsamtal?" @@ -33092,6 +33191,16 @@ msgstr "Startdatum måste vara tidigare än slutdatum." msgid "Start drag and drop" msgstr "Börja dra och släpp" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Starta i P2P-läge" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Starta i P2P-läge?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Starta minimerad" msgid "Start over" msgstr "Börja om" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Starta P2P-samtal" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Börja sökningar med <0>{peoplePrefix} <1>{textChannelPrefix} < msgid "Start speaking" msgstr "Starta uppläsning" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Starta vanligt samtal" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Statuspanel" msgid "Status:" msgstr "Status:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Behåll" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Stopp" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Sluta vara en meddelandekanal?" @@ -33921,6 +34045,11 @@ msgstr "Växla till föregående community eller DM" msgid "Switch to single column" msgstr "Växla till en kolumn" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Byt till vanligt samtal" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Gruppen lossad" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Grupp-DM-konversationen lossad" @@ -37727,6 +37856,11 @@ msgstr "Overifierad e-post" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Upp till {DISCOVERY_MAX_TAGS} taggar hjälper andra att hitta din community. De visas i sökningen i Upptäck." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Upp till {maxParticipants, plural, one {# person} other {# personer}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Laddar upp emojier" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Videosändning:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Videoinställningar" @@ -39570,7 +39704,7 @@ msgstr "Röstinspelning stöds inte i den här webbläsaren." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Röstregion" @@ -39582,7 +39716,7 @@ msgstr "Meddelandefält för återställning av röstsession" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Röstinställningar" diff --git a/fluxer_app/src/features/i18n/locales/th/messages.po b/fluxer_app/src/features/i18n/locales/th/messages.po index 293d91825..91198a83d 100644 --- a/fluxer_app/src/features/i18n/locales/th/messages.po +++ b/fluxer_app/src/features/i18n/locales/th/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {ข้อความที่ถูกบล็อก # รายการ} other {ข้อความที่ถูกบล็อก # รายการ}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# ช่องทางติดตาม} other {# ช่องทางติดตาม}} ช่องทางนี้ การแปลงเป็นช่องทางข้อความจะลบการติดตามเหล่านั้นออก" @@ -2720,6 +2720,11 @@ msgstr "อิโมจิเริ่มต้น คุณสามารถ msgid "A description is required." msgstr "ต้องระบุคำอธิบาย" +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "ไม่สามารถเชื่อมต่อโดยตรงได้ คุณจึงไม่ได้ยินหรือเห็นกัน" + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "การฝังเนื้อหาแบบสมบูรณ์ห msgid "A separate profile for each community" msgstr "โปรไฟล์แยกสำหรับแต่ละชุมชน" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "การโทรแบบปกติจะผ่านเซิร์ฟเวอร์และใช้งานได้กับทุกคน" + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "ผู้ดูแลระบบหยุดคำเชิญชั่ #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "ทุกคนสามารถขอบัญชีได้ แต msgid "Anyone in my communities" msgstr "ทุกคนในชุมชนของฉัน" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "หากใครไม่สามารถเชื่อมต่อโดยตรงได้ จะเข้าร่วมไม่ได้จนกว่าจะมีคนเปลี่ยนสายเป็นการโทรแบบมาตรฐาน" + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "ทุกคนที่เจอคุณสามารถส่งคำขอได้ ปิดสิ่งนี้เพื่อเลือกกลุ่มด้านล่าง" @@ -5112,6 +5127,16 @@ msgstr "เสียง" msgid "Audio & video" msgstr "เสียงและวิดีโอ" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "เสียงและวิดีโอจะส่งตรงระหว่างผู้เข้าร่วม สูงสุด {maxParticipants, plural, one {# คน} other {# คน}} ทุกคนจะตกลงกันก่อนเข้าร่วมและสามารถเห็นที่อยู่ IP ของกันและกันได้" + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "เสียงและวิดีโอจะส่งตรงระหว่างผู้เข้าร่วม" + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "คำแนะนำการเติมข้อความอัต #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "อัตโนมัติ" @@ -6936,15 +6961,15 @@ msgstr "ไม่สามารถลบบัญชีได้" msgid "Capture" msgstr "บันทึกเสียง" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "บันทึกเสียงจากแอป" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "บันทึกเสียงเดสก์ท็อป" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "บันทึกเสียงจากอุปกรณ์" @@ -7017,7 +7042,7 @@ msgstr "คัดลอก ID หมวดหมู่แล้ว" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "ชื่อหมวดหมู่" @@ -7554,7 +7579,7 @@ msgstr "ส่วนต่างๆ ของรายละเอียดช #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "คัดลอก ID ช่องแล้ว" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "ลิงก์ช่อง" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "คัดลอกลิงก์ช่องแล้ว" @@ -7604,7 +7629,7 @@ msgstr "การจัดการช่อง" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "ช่องซิงค์กับหมวดหมู่หลักแล้ว" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "หัวข้อช่องยาวเกินไป" @@ -7668,8 +7693,8 @@ msgstr "เลือกประเภทช่อง" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "อัปเดตช่องแล้ว" @@ -9421,7 +9446,7 @@ msgstr "ยืนยันก่อนเข้าร่วมช่องเส #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "ยืนยันก่อนเริ่มการโทร" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "การสนทนา" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "แปลง" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "แปลงช่องเป็นช่องประกาศ" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "การแปลงช่องนี้เป็นช่องข้อความจะลบทุกช่องที่ติดตามช่องนี้" @@ -9949,7 +9974,7 @@ msgstr "คัดลอก ID ช่อง" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "คัดลอกลิงก์ช่อง" @@ -10379,13 +10404,18 @@ msgstr "ลงชื่อเข้าใช้ไม่สำเร็จ โ msgid "Couldn't connect to {domain}. Try again." msgstr "ไม่สามารถเชื่อมต่อกับ {domain} ได้ ลองอีกครั้ง" +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "ไม่สามารถเชื่อมต่อกับ {names} ได้" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "เชื่อมต่อเสียงไม่ได้ โปรดลองอีกครั้ง" #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "ไม่สามารถแปลงช่องนี้ได้" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "ดาวน์โหลดการอัปเดตเดสก์ท็อปไม่ได้" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "ไม่สามารถทำซ้ำช่องนี้ได้" @@ -10511,6 +10541,11 @@ msgstr "ไม่สามารถเพิกเฉยคำขอเป็น msgid "Couldn't join community" msgstr "ไม่สามารถเข้าร่วมคอมมูนิตี้ได้" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "เข้าร่วมการโทรแบบ P2P ไม่สำเร็จ ลองอีกครั้ง" + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "ไม่สามารถเปิดพอร์ทัลการเรียกเก็บเงินได้" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "ปักหมุดการสนทนานี้ไม่ได้" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "ไม่สามารถเรียกใช้คำสั่งนั้นได้: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "ไม่สามารถบันทึกการตั้งค่าช่องได้" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "ไม่สามารถเลิกติดตามช่องนี้ได้" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "ไม่สามารถเลิกปักหมุดการสนทนานี้ได้" @@ -11374,7 +11409,7 @@ msgstr "กำหนดเอง" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "กำหนดเอง" @@ -11938,7 +11973,7 @@ msgstr "ค่าเริ่มต้น" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "ลบ \"{stickerName}\" ใช่หรือไม่ การดำ #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "ลบ {channelLabel} ใช่หรือไม่ การดำเนินการนี้จะยกเลิกไม่ได้" @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "เสียงอุปกรณ์ ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "เสียงอุปกรณ์เท่านั้น" @@ -12694,7 +12729,7 @@ msgstr "รายละเอียดอุปกรณ์" msgid "Device revoked" msgstr "ยกเลิกอุปกรณ์แล้ว" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "การเลือกอุปกรณ์" @@ -12758,6 +12793,11 @@ msgstr "ขนาดและจำนวนสมาชิกต่างกั msgid "Dim strikethrough text" msgstr "หรี่ข้อความขีดทับ" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "การเชื่อมต่อโดยตรง สื่อถูกเข้ารหัสระหว่างผู้เข้าร่วม" + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "แถบด้านข้าง DM" msgid "DNS TXT record" msgstr "ระเบียน DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "ไม่ต้องถามอีกต่อไป ตกลงเสมอ" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "ทำซ้ำ" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "ทำซ้ำช่อง" @@ -13825,7 +13870,7 @@ msgstr "แก้ไขหมวดหมู่" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "แก้ไขช่อง" @@ -15004,7 +15049,7 @@ msgstr "ปุ่ม Esc" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "ปุ่ม Esc ออกจากโหมดแป้นพิมพ์" @@ -15047,6 +15092,11 @@ msgstr "ทุกชุมชนที่ติดตามช่องนี้ msgid "Everyone" msgstr "ทุกคน" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "ทุกคนในสายจะได้รับคุณภาพสตรีม Plutonium ฟรี: ความละเอียดสูงสุด 4K และ 60 fps" + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "ได้รับสิทธิ์ {permissions} แล้ว" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "ให้สิทธิ์ทั้งหมดและข้ามข้อจำกัดของช่อง มีความละเอียดอ่อนสูง" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "เลือกแบบละเอียด" @@ -17417,7 +17467,7 @@ msgstr "กรีก" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "มุมมองตาราง" @@ -17909,7 +17959,7 @@ msgstr "ซ่อนผู้เข้าร่วม" msgid "Hide password" msgstr "ซ่อนรหัสผ่าน" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "ซ่อนภาพตัวอย่าง" @@ -18322,11 +18372,11 @@ msgstr "ไม่สนใจ" msgid "Ignore friend request" msgstr "ไม่สนใจคำขอเป็นเพื่อน" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "ไม่สนใจอุปกรณ์ฮาร์ดแวร์" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "ไม่สนใจสื่อนำเข้า" @@ -18357,7 +18407,7 @@ msgstr "ไม่สนใจคำขอเป็นเพื่อนจาก msgid "Ignore this channel's slowmode cooldown." msgstr "ข้ามช่วงรอของโหมดจำกัดเวลาในช่องนี้" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "ไม่สนใจอุปกรณ์เสมือน" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "อุปกรณ์อินพุต" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "ระดับอินพุต" @@ -18865,7 +18915,7 @@ msgstr "การควบคุมอินพุต" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "โปรไฟล์อินพุต" @@ -19431,6 +19481,12 @@ msgstr "จิตเตอร์:" msgid "Join" msgstr "เข้าร่วม" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "เข้าร่วม" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "เข้าร่วมคอมมูนิตี้" msgid "Join a community to unlock stickers." msgstr "เข้าร่วมคอมมูนิตี้เพื่อปลดล็อกสติกเกอร์" +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "เข้าร่วมการโทรแบบ P2P ไหม" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "วิธีเข้าร่วม" msgid "Join on this device" msgstr "เข้าร่วมบนอุปกรณ์นี้" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "เข้าร่วมการโทรแบบ P2P โดยไม่ต้องยืนยัน" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "ออกจาก {threadName} ไหม? เป็นส่วนต msgid "Leave blank for a random username" msgstr "เว้นว่างไว้เพื่อสุ่มชื่อผู้ใช้" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "ออกจากการโทร" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "ให้ CSS ที่กำหนดเองแสดงพื้นที่หน้าต่างโปร่งใสหลังจากรีสตาร์ทแอปเดสก์ท็อป" #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "ให้ชุมชนอื่นติดตามช่องนี้และรับสำเนาข้อความที่คุณเผยแพร่" @@ -20729,6 +20797,11 @@ msgstr "ลุยเซียนา" msgid "Low" msgstr "ต่ำ" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "ความหน่วงต่ำ" + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "มิชิแกน" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "มินนิโซตา" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "กลับด้านกล้อง" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "สติกเกอร์สุดเจ๋งของฉัน" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "หมวดหมู่ของฉัน" @@ -24444,7 +24517,7 @@ msgstr "ดำเนินการต่อก็ต่อเมื่อคุ msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "รับเฉพาะบัตรเครดิตเท่านั้น ไม่สามารถใช้บัตรเดบิตและบัตรเติมเงินในการตรวจสอบเนื้อหาสำหรับผู้ใหญ่ได้" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "ลำโพงเริ่มต้นเท่านั้น" @@ -24522,7 +24595,7 @@ msgstr "เฉพาะสมาชิกและบทบาทที่เล msgid "Only show message previews for DMs with unread messages" msgstr "แสดงตัวอย่างข้อความเฉพาะในข้อความส่วนตัวที่มีข้อความยังไม่ได้อ่าน" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "เฉพาะลำโพง" @@ -24759,7 +24832,7 @@ msgstr "เปิดรายละเอียดช่อง {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "เปิดแชท" @@ -24840,7 +24913,7 @@ msgstr "เปิดอิโมจิในเบราว์เซอร์" msgid "Open emoji picker" msgstr "เปิดตัวเลือกอิโมจิ" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "เปิด Event Sink" @@ -25280,6 +25353,16 @@ msgstr "กระทู้ที่ใช้งานอื่นๆ" msgid "Other devices" msgstr "อุปกรณ์อื่น" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "ผู้เข้าร่วมคนอื่น ๆ สามารถเห็นที่อยู่ IP ของคุณได้" + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "ผู้เข้าร่วมคนอื่น ๆ ในการโทรแบบ P2P จะเห็นที่อยู่ IP ของคุณ" + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "ไม่สามารถโอนสิทธิ์การเป็ msgid "Ownership transferred" msgstr "โอนสิทธิ์การเป็นเจ้าของแล้ว" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "ขณะนี้ไม่สามารถโทรแบบ P2P ได้" + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "การโทรแบบ P2P เท่านั้น" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "ปักหมุดกลุ่มแล้ว" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "ปักหมุดแชทกลุ่มแล้ว" @@ -26536,7 +26635,7 @@ msgstr "ปุ่มดูตัวอย่าง" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "ดูตัวอย่างกล้อง" @@ -27107,7 +27206,7 @@ msgstr "ปุ่มกดเพื่อพูด" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "กดเพื่อพูด" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "ย่อข้อความแล้วลองอีกครั้ง" #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "ย่อหัวข้อแล้วลองอีกครั้ง" @@ -31488,7 +31587,7 @@ msgstr "แสดงการควบคุมระดับเสียงก #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "แสดงปุ่มลัดเมนูบริบท" @@ -31573,7 +31672,7 @@ msgstr "แสดงเอฟเฟกต์เสียงให้น้อย #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "แสดงวงแหวนโฟกัสบนช่องข้อความแชท" @@ -31701,7 +31800,7 @@ msgstr "แสดงช่องที่ปิดเสียง" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "แสดงกล้องของฉัน" @@ -31742,7 +31841,7 @@ msgstr "แสดงโพสต์ใหม่เป็นยังไม่อ #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "แสดงผู้เข้าร่วมที่ไม่ได้เปิดกล้อง" @@ -32144,7 +32243,7 @@ msgstr "โทรแบบไร้เสียง" msgid "Silent calls" msgstr "สายเรียกเข้าแบบไม่มีเสียง" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "เงียบเมื่ออุปกรณ์บันทึกไม่มีอินพุตเสียงของตัวเอง" @@ -32655,8 +32754,8 @@ msgstr "เกิดข้อผิดพลาด" #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "เกิดข้อผิดพลาด โปรดลองอีกครั้งในภายหลัง" @@ -32750,7 +32849,7 @@ msgstr "เสียง" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "ต้นฉบับ" @@ -32859,7 +32958,7 @@ msgstr "พูดได้เลย กดหยุดเมื่อเสร #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "ลำโพง" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "เริ่มแชทกลุ่ม" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "เริ่มวิดีโอคอลไหม" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "เริ่มการโทรด้วยเสียงไหม" @@ -33092,6 +33191,16 @@ msgstr "วันที่เริ่มต้นต้องเร็วกว msgid "Start drag and drop" msgstr "เริ่มลากและวาง" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "เริ่มในโหมด P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "เริ่มในโหมด P2P ไหม" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "เริ่มแบบย่อ" msgid "Start over" msgstr "เริ่มใหม่" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "เริ่มการโทรแบบ P2P" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "เริ่มค้นหาด้วย <0>{peoplePrefix} <1>{tex msgid "Start speaking" msgstr "เริ่มพูด" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "เริ่มการโทรแบบมาตรฐาน" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "แผงสถานะ" msgid "Status:" msgstr "สถานะ:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "อยู่ต่อ" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "หยุด" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "หยุดเป็นช่องประกาศหรือไม่" @@ -33921,6 +34045,11 @@ msgstr "สลับไปคอมมูนิตี้หรือข้อค msgid "Switch to single column" msgstr "เปลี่ยนเป็นคอลัมน์เดียว" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "สลับไปเป็นการโทรแบบมาตรฐาน" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "เลิกปักหมุดกลุ่มแล้ว" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "เลิกปักหมุดแชทกลุ่มแล้ว" @@ -37727,6 +37856,11 @@ msgstr "อีเมลที่ยังไม่ได้ยืนยัน" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "เพิ่มแท็กได้สูงสุด {DISCOVERY_MAX_TAGS} แท็กเพื่อให้คนอื่นค้นหาคอมมูนิตี้ของคุณเจอ แท็กจะแสดงในการค้นหาของ Discovery" +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "สูงสุด {maxParticipants, plural, one {# คน} other {# คน}}" + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "กำลังอัปโหลดอิโมจิ" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "ส่งวิดีโอ:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "การตั้งค่าวิดีโอ" @@ -39570,7 +39704,7 @@ msgstr "เบราว์เซอร์นี้ไม่รองรับก #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "ภูมิภาคเสียง" @@ -39582,7 +39716,7 @@ msgstr "แถบแจ้งเตือนการกู้คืนเซส #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "การตั้งค่าเสียง" diff --git a/fluxer_app/src/features/i18n/locales/tr/messages.po b/fluxer_app/src/features/i18n/locales/tr/messages.po index 3e3b06222..b8ee79b21 100644 --- a/fluxer_app/src/features/i18n/locales/tr/messages.po +++ b/fluxer_app/src/features/i18n/locales/tr/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# engellenen mesaj} other {# engellenen mesaj}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "Bu kanalı {count, plural, one {# kanal} other {# kanal}} takip ediyor. Metin kanalına dönüştürmek bu takipleri kaldırır." @@ -2720,6 +2720,11 @@ msgstr "Varsayılan bir emoji. Bunu {productName} üzerinde istediğin yerde kul msgid "A description is required." msgstr "Açıklama gerekli." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Doğrudan bir bağlantı kurulamadı. Birbirinizi duyamıyor veya göremiyorsunuz." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "zengin bir gömülü içerik veya bağlantı önizlemesi (yüklenen bir msgid "A separate profile for each community" msgstr "Her topluluk için ayrı bir profil" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Normal bir arama sunucu üzerinden yapılır ve herkes için çalışır." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Yöneticiler davetleri duraklattı, şu anda katılamazsınız." #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Herkes hesap isteyebilir ancak bir yönetici onaylamalıdır." msgid "Anyone in my communities" msgstr "Topluluklarımdaki herkes" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Doğrudan bağlanamayan herkes, biri aramayı standart bir aramaya geçirene kadar katılamaz." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Seni bulan herkes istek gönderebilir. Aşağıdaki grupları seçmek için bunu kapat." @@ -5112,6 +5127,16 @@ msgstr "Ses" msgid "Audio & video" msgstr "Ses ve görüntü" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Sesli ve görüntülü görüşmeler doğrudan katılımcılar arasında yapılır, en fazla {maxParticipants, plural, one {# kişi} other {# kişi}} olabilir. Herkes katılmadan önce onay verir ve birbirlerinin IP adreslerini görebilir." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Ses ve görüntü doğrudan katılımcılar arasında iletilir." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Otomatik tamamlama önerisi: {label}, {resultCount} sonuçtan {selectedO #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Otomatik" @@ -6936,15 +6961,15 @@ msgstr "Hesap silinemiyor" msgid "Capture" msgstr "Yakala" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Uygulama sesini yakala" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Masaüstü sesini yakala" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Cihaz sesini yakala" @@ -7017,7 +7042,7 @@ msgstr "Kategori kimliği kopyalandı" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Kategori adı" @@ -7554,7 +7579,7 @@ msgstr "Kanal ayrıntıları bölümleri" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Kanal kimliği kopyalandı" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "kanal bağlantısı" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Kanal bağlantısı kopyalandı" @@ -7604,7 +7629,7 @@ msgstr "Kanal yönetimi" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Kanal üst kategoriyle senkronize edildi" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Kanal konusu çok uzun." @@ -7668,8 +7693,8 @@ msgstr "Kanal türü seçimi" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Kanal güncellendi" @@ -9421,7 +9446,7 @@ msgstr "Sesli kanallara katılmadan önce onayla" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Aramaları başlatmadan önce onayla" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Sohbetler" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Dönüştür" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Kanalı duyuru kanalı olarak değiştirdi" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Bu kanalı bir metin kanalına dönüştürmek, onu takip eden tüm kanalları kaldırır." @@ -9949,7 +9974,7 @@ msgstr "Kanal kimliğini kopyala" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Kanal bağlantısını kopyala" @@ -10379,13 +10404,18 @@ msgstr "Oturum açılamadı. Tekrar deneyin." msgid "Couldn't connect to {domain}. Try again." msgstr "{domain}'e bağlanılamadı. Tekrar dene." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "{names} ile bağlantı kurulamadı" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Sesli sohbata bağlanılamadı. Lütfen tekrar deneyin." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Bu kanal dönüştürülemedi" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Masaüstü güncellemesi indirilemedi" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Bu kanal çoğaltılamadı" @@ -10511,6 +10541,11 @@ msgstr "Arkadaşlık isteği yok sayılamadı. Tekrar deneyin." msgid "Couldn't join community" msgstr "Topluluğa katılamadınız" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "P2P aramasına katılamadık. Tekrar deneyin." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Faturalandırma portalı açılamadı" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Bu sohbet sabitlenemedi" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Bu komut çalıştırılamadı: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Kanal ayarları kaydedilemedi" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Bu kanaldan çıkılamadı" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Bu sohbetin sabitlemesi kaldırılamadı" @@ -11374,7 +11409,7 @@ msgstr "Özel" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Özel" @@ -11938,7 +11973,7 @@ msgstr "Varsayılan" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "\"{stickerName}\" silinsin mi? Bu işlem geri alınamaz." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "{channelLabel} silinsin mi? Bu işlem geri alınamaz." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Cihaz sesi ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Yalnızca cihaz sesi" @@ -12694,7 +12729,7 @@ msgstr "Cihaz ayrıntıları" msgid "Device revoked" msgstr "Cihazın erişimi iptal edildi" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Cihaz seçimi" @@ -12758,6 +12793,11 @@ msgstr "Farklı boyutlar ve üye sayıları" msgid "Dim strikethrough text" msgstr "Üstü çizili metni soluklaştır" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Doğrudan bağlantı. Medya katılımcılar arasında şifrelenir." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "DM kenar çubuğu" msgid "DNS TXT record" msgstr "DNS TXT kaydı" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Bir daha sorma, her zaman kabul et" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Çoğalt" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Kanalı çoğalt" @@ -13825,7 +13870,7 @@ msgstr "Kategoriyi düzenle" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Kanalı düzenle" @@ -15004,7 +15049,7 @@ msgstr "Esc tuşu" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Esc tuşu klavye modundan çıkar" @@ -15047,6 +15092,11 @@ msgstr "Bu kanalı takip eden her topluluk bir kopya alır. Daha sonra düzenler msgid "Everyone" msgstr "Herkes" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Aramadaki herkes, 4K'ya kadar kaynak çözünürlük ve 60 fps ile Plutonium yayın kalitesini ücretsiz alır." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "{permissions} verildi" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Tüm izinleri verir ve kanal kısıtlamalarını atlar. Çok hassastır." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Ayrıntılı seçim" @@ -17417,7 +17467,7 @@ msgstr "Yunanca" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Izgara görünümü" @@ -17909,7 +17959,7 @@ msgstr "Katılımcıları gizle" msgid "Hide password" msgstr "Parolayı gizle" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Önizleme küçük resmini gizle" @@ -18322,11 +18372,11 @@ msgstr "Yok say" msgid "Ignore friend request" msgstr "Arkadaşlık isteğini yok say" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Donanım aygıtlarını yok say" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Giriş medyasını yok say" @@ -18357,7 +18407,7 @@ msgstr "{displayName} adlı kişiden gelen arkadaşlık isteği yok sayılsın m msgid "Ignore this channel's slowmode cooldown." msgstr "Bu kanalın yavaş mod sınırlamasını yok sayın." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Sanalı yok say" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Giriş aygıtı" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Giriş seviyesi" @@ -18865,7 +18915,7 @@ msgstr "Girdi İzleme" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Giriş profili" @@ -19431,6 +19481,12 @@ msgstr "Titreşim:" msgid "Join" msgstr "Katıl" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Katıl" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Bir topluluğa katıl" msgid "Join a community to unlock stickers." msgstr "Çıkartmaların kilidini açmak için bir topluluğa katılın." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "P2P aramasına katıl?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Katılma yöntemi" msgid "Join on this device" msgstr "Bu cihazdan katıl" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "P2P aramalarına sormadan katıl" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "{threadName} konusundan ayrıl? Burası özel, bu yüzden biri seni tekr msgid "Leave blank for a random username" msgstr "Rastgele bir kullanıcı adı için boş bırakın" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Aramadan ayrıl" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Masaüstü uygulamasını yeniden başlattıktan sonra özel CSS'in şeffaf pencere bölgelerini göstermesine izin ver." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Diğer toplulukların bu kanalı takip etmesini ve yayınladıklarınızın kopyalarını almasını sağlar." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Düşük" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Daha düşük gecikme süresi." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Kamerayı yansıt" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Harika çıkartmam" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Kategorim" @@ -24444,7 +24517,7 @@ msgstr "Yalnızca kontrol ettiğiniz bir cihazda bu oturum açmayı başlattıys msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Yalnızca kredi kartları kabul edilir. Banka ve ön ödemeli kartlar yetişkin içeriği kontrolü için kullanılamaz." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Yalnızca varsayılan hoparlörler" @@ -24522,7 +24595,7 @@ msgstr "Yalnızca seçilen üyeler ve roller bu kanalı görebilecek." msgid "Only show message previews for DMs with unread messages" msgstr "Yalnızca okunmamış mesajları olan DM'ler için mesaj önizlemelerini göster" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Yalnızca hoparlörler" @@ -24759,7 +24832,7 @@ msgstr "{channelName} için kanal ayrıntılarını aç" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Sohbeti aç" @@ -24840,7 +24913,7 @@ msgstr "Emojiyi tarayıcıda aç" msgid "Open emoji picker" msgstr "Emoji seçiciyi aç" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Olay sink'ini aç" @@ -25280,6 +25353,16 @@ msgstr "Diğer aktif başlıklar" msgid "Other devices" msgstr "Diğer cihazlar" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Diğer katılımcılar IP adresinizi görebilir." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "P2P aramada diğer katılımcılar IP adresinizi görebilir." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Sahiplik şu anda <0>{username} kişisine devredilemedi." msgid "Ownership transferred" msgstr "Sahiplik devredildi" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Şu anda eşler arası aramalar kullanılamıyor." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Yalnızca P2P aramalar" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Grup sabitlendi" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Grup DM'i sabitlendi" @@ -26536,7 +26635,7 @@ msgstr "Önizleme düğmesi" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Kamerayı önizle" @@ -27107,7 +27206,7 @@ msgstr "Bas konuş tuşu" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Bas konuş" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Mesajı kısaltıp tekrar deneyin." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Konuyu kısaltıp tekrar deneyin." @@ -31488,7 +31587,7 @@ msgstr "Bağlantı ses seviyesi denetimlerini göster" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Bağlam menüsü kısayollarını göster" @@ -31573,7 +31672,7 @@ msgstr "Daha az ses efekti göster" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Sohbet metin alanında odak halkasını göster" @@ -31701,7 +31800,7 @@ msgstr "Sessize alınmış kanalları göster" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Kendi kameramı göster" @@ -31742,7 +31841,7 @@ msgstr "Yeni gönderileri okunmamış olarak göster" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Videosu olmayan katılımcıları göster" @@ -32144,7 +32243,7 @@ msgstr "Sessiz arama" msgid "Silent calls" msgstr "Sessiz aramalar" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Cihazın kendi ses girişi olmadığında sessiz" @@ -32655,8 +32754,8 @@ msgstr "Bir şeyler ters gitti." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Bir şeyler ters gitti. Lütfen birazdan tekrar deneyin." @@ -32750,7 +32849,7 @@ msgstr "Sesler" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Kaynak" @@ -32859,7 +32958,7 @@ msgstr "Şimdi konuşun. Bitirdiğinizde Durdur'a basın. Daha sonra kırpabilir #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Hoparlör" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Grup DM başlat" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Görüntülü arama başlatılsın mı?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Sesli arama başlatılsın mı?" @@ -33092,6 +33191,16 @@ msgstr "Başlangıç tarihi bitiş tarihinden önce olmalı." msgid "Start drag and drop" msgstr "Sürükleyip bırakmayı başlat" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "P2P modunda başlat" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "P2P modunda başla?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Küçültülmüş olarak başlat" msgid "Start over" msgstr "Baştan başla" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "P2P araması başlat" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Aramalara <0>{peoplePrefix}, <1>{textChannelPrefix}, <2>{voiceCh msgid "Start speaking" msgstr "Konuşmayı başlat" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Standart aramayı başlat" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Durum paneli" msgid "Status:" msgstr "Durum:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Kaldır" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Durdur" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Duyuru kanalı olmayı durdur?" @@ -33921,6 +34045,11 @@ msgstr "Önceki topluluğa veya DM'lere geç" msgid "Switch to single column" msgstr "Tek sütuna geç" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Standart aramaya geç" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Grubun sabitlemesi kaldırıldı" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Grup DM'inin sabitlemesi kaldırıldı" @@ -37727,6 +37856,11 @@ msgstr "Doğrulanmamış e-posta" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "En fazla {DISCOVERY_MAX_TAGS} etiket, insanların topluluğunuzu bulmasına yardımcı olur. Keşfet aramasında görünürler." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "En fazla {maxParticipants, plural, one {# kişi} other {# kişi}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Emojiler yükleniyor" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Video gönderimi:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Video ayarları" @@ -39570,7 +39704,7 @@ msgstr "Ses kaydı bu tarayıcıda desteklenmiyor." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Ses bölgesi" @@ -39582,7 +39716,7 @@ msgstr "Ses oturumu geri yükleme bildirim çubuğu" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Ses ayarları" diff --git a/fluxer_app/src/features/i18n/locales/uk/messages.po b/fluxer_app/src/features/i18n/locales/uk/messages.po index 89b0e9ee1..b86d28788 100644 --- a/fluxer_app/src/features/i18n/locales/uk/messages.po +++ b/fluxer_app/src/features/i18n/locales/uk/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# заблоковане повідомлення} few {# заблоковані повідомлення} many {# заблокованих повідомлень} other {# заблокованих повідомлень}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# канал підписаний} few {# канали підписані} many {# каналів підписано} other {# каналів підписано}} на цей канал. Перетворення його на текстовий канал скасує ці підписки." @@ -2720,6 +2720,11 @@ msgstr "Стандартний емодзі. Ви можете використ msgid "A description is required." msgstr "Потрібен опис." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Не вдалося встановити пряме з'єднання. Ви не можете чути або бачити одне одного." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "розширений вбудований вміст або попере msgid "A separate profile for each community" msgstr "Окремий профіль для кожної спільноти" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Звичайний виклик проходить через сервер і працює для всіх." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Адміністратори призупинили запрошення #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Будь-хто може подати запит на обліковий msgid "Anyone in my communities" msgstr "Будь-хто з моїх спільнот" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Якщо хтось не може підключитися напряму, він не зможе взяти участь, доки хтось не перемкне дзвінок на стандартний." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Будь-хто, хто вас знайде, може надіслати запит. Вимкніть це, щоб вибрати групи нижче." @@ -5112,6 +5127,16 @@ msgstr "Аудіо" msgid "Audio & video" msgstr "Аудіо та відео" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Аудіо та відео передаються безпосередньо між учасниками, до {maxParticipants, plural, one {# особи} other {# осіб}}. Усі погоджуються перед приєднанням і можуть бачити IP-адресу один одного." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Аудіо та відео надсилаються безпосередньо між учасниками." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Пропозиція автозаповнення: {label}, {selectedOpt #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Автоматично" @@ -6936,15 +6961,15 @@ msgstr "Неможливо видалити обліковий запис" msgid "Capture" msgstr "Захоплення" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Захоплювати звук застосунку" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Захоплювати звук робочого столу" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Захоплювати звук пристрою" @@ -7017,7 +7042,7 @@ msgstr "Ідентифікатор категорії скопійовано" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Назва категорії" @@ -7554,7 +7579,7 @@ msgstr "Розділи інформації про канал" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Ідентифікатор каналу скопійовано" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "посилання на канал" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Посилання на канал скопійовано" @@ -7604,7 +7629,7 @@ msgstr "Керування каналами" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Канал синхронізовано з батьківською категорією" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Тема каналу задовга." @@ -7668,8 +7693,8 @@ msgstr "Вибір типу каналу" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Канал оновлено" @@ -9421,7 +9446,7 @@ msgstr "Запитувати підтвердження перед приєдн #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Запитувати підтвердження перед дзвінками" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Розмови" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Перетворити" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Канал було перетворено на канал оголошень" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Перетворення цього каналу на текстовий видалить усі канали, що його відстежують." @@ -9949,7 +9974,7 @@ msgstr "Копіювати ID каналу" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Копіювати посилання на канал" @@ -10379,13 +10404,18 @@ msgstr "Не вдалося завершити вхід. Спробуйте ще msgid "Couldn't connect to {domain}. Try again." msgstr "Не вдалося підключитися до {domain}. Спробуйте ще раз." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Не вдалося підключитися до {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Не вдалося підключитися до голосового зв'язку. Спробуйте ще раз." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Не вдалося конвертувати цей канал" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Не вдалося завантажити оновлення для комп'ютера" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Не вдалося продублювати цей канал" @@ -10511,6 +10541,11 @@ msgstr "Не вдалося проігнорувати запит на дода msgid "Couldn't join community" msgstr "Не вдалося приєднатися до спільноти" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Не вдалося приєднатися до P2P-виклику. Спробуйте ще раз." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Не вдалося відкрити платіжний портал" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Не вдалося закріпити цю розмову" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Не вдалося виконати команду: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Не вдалося зберегти налаштування каналу" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Не вдалося скасувати підписку на цей канал" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Не вдалося відкріпити цю розмову" @@ -11374,7 +11409,7 @@ msgstr "Власне джерело" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Власна" @@ -11938,7 +11973,7 @@ msgstr "Типово" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Видалити «{stickerName}»? Цю дію не можна скас #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Видалити {channelLabel}? Цю дію не можна скасувати." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Аудіо пристрою ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Аудіо пристрою" @@ -12694,7 +12729,7 @@ msgstr "Деталі пристрою" msgid "Device revoked" msgstr "Пристрій відкликано" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Вибір пристрою" @@ -12758,6 +12793,11 @@ msgstr "Різні розміри та кількість учасників" msgid "Dim strikethrough text" msgstr "Приглушити закреслений текст" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Пряме з'єднання. Медіа зашифроване між учасниками." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Бічна панель приватних чатів" msgid "DNS TXT record" msgstr "Запис DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Не питати знову, завжди погоджуватися" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Дублювати" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Дублювати канал" @@ -13825,7 +13870,7 @@ msgstr "Редагувати категорію" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Редагувати канал" @@ -15004,7 +15049,7 @@ msgstr "Клавіша Esc" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Клавіша Esc вимикає режим клавіатури" @@ -15047,6 +15092,11 @@ msgstr "Кожна спільнота, що підписана на цей ка msgid "Everyone" msgstr "Усі" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Усі учасники дзвінка безкоштовно отримують якість потоку Plutonium: до 4K вихідної роздільної здатності та 60 кадрів на секунду." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Надано {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Надає всі дозволи та обходить обмеження каналів. Дуже ризиковано." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Детальний вибір" @@ -17417,7 +17467,7 @@ msgstr "Грецька" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Вигляд сіткою" @@ -17909,7 +17959,7 @@ msgstr "Сховати учасників" msgid "Hide password" msgstr "Сховати пароль" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Сховати мініатюру попереднього перегляду" @@ -18322,11 +18372,11 @@ msgstr "Ігнорувати" msgid "Ignore friend request" msgstr "Ігнорувати запит на додавання в друзі" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Ігнорувати апаратні пристрої" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Ігнорувати вхідні медіа" @@ -18357,7 +18407,7 @@ msgstr "Ігнорувати запит на додавання в друзі в msgid "Ignore this channel's slowmode cooldown." msgstr "Ігнорувати обмеження повільного режиму цього каналу." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Ігнорувати віртуальні" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Пристрій введення" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Рівень вхідного сигналу" @@ -18865,7 +18915,7 @@ msgstr "Моніторинг введення" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Профіль введення" @@ -19431,6 +19481,12 @@ msgstr "Джитер:" msgid "Join" msgstr "Приєднатися" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Приєднатися" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Приєднатися до спільноти" msgid "Join a community to unlock stickers." msgstr "Щоб розблокувати стікери, приєднайтеся до спільноти." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Приєднатися до P2P-дзвінка?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Спосіб приєднання" msgid "Join on this device" msgstr "Приєднатися з цього пристрою" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Приєднуватися до P2P-дзвінків без підтвердження" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Вийти з {threadName}? Це приватна гілка, тому msgid "Leave blank for a random username" msgstr "Залиште порожнім для випадкового імені користувача" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Вийти з дзвінка" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Дозволяє власному CSS показувати прозорі області вікна після перезапуску десктопного застосунку." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Дозволяє іншим спільнотам підписуватися на цей канал і отримувати копії ваших публікацій." @@ -20729,6 +20797,11 @@ msgstr "Луїзіана" msgid "Low" msgstr "Низький" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Менша затримка." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Мічиган" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Міннесота" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Віддзеркалити камеру" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Мій чудовий стікер" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Моя категорія" @@ -24444,7 +24517,7 @@ msgstr "Продовжуйте, лише якщо ви розпочали цей msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Приймаються лише кредитні картки. Дебетові та передплачені картки не підходять для перевірки доступу до контенту для дорослих." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Лише стандартні динаміки" @@ -24522,7 +24595,7 @@ msgstr "Цей канал зможуть переглядати лише виб msgid "Only show message previews for DMs with unread messages" msgstr "Показувати попередній перегляд повідомлень лише для приватних чатів із непрочитаними повідомленнями" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Лише динаміки" @@ -24759,7 +24832,7 @@ msgstr "Відкрити деталі каналу «{channelName}»" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Відкрити чат" @@ -24840,7 +24913,7 @@ msgstr "Відкрити емоджі в браузері" msgid "Open emoji picker" msgstr "Відкрити панель емоджі" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Відкрити приймач подій" @@ -25280,6 +25353,16 @@ msgstr "Інші активні теми" msgid "Other devices" msgstr "Інші пристрої" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Інші учасники можуть бачити вашу IP-адресу." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Інші учасники P2P-виклику можуть бачити вашу IP-адресу." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Наразі не вдалося передати право власн msgid "Ownership transferred" msgstr "Право власності передано" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "P2P-дзвінки зараз недоступні." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Лише прямі дзвінки" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Закріплена група" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Закріплений груповий чат" @@ -26536,7 +26635,7 @@ msgstr "Кнопка попереднього перегляду" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Попередній перегляд камери" @@ -27107,7 +27206,7 @@ msgstr "Клавіша «Натисни і говори»" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Натисни і говори" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Скоротіть повідомлення та спробуйте ще раз." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Скоротіть тему та спробуйте ще раз." @@ -31488,7 +31587,7 @@ msgstr "Показувати керування гучністю підключ #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Показувати комбінації клавіш контекстного меню" @@ -31573,7 +31672,7 @@ msgstr "Показати менше звукових ефектів" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Показувати рамку фокусу в полі введення чату" @@ -31701,7 +31800,7 @@ msgstr "Показати канали з вимкненими сповіщенн #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Показувати мою камеру" @@ -31742,7 +31841,7 @@ msgstr "Позначати нові дописи як непрочитані" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Показувати учасників без відео" @@ -32144,7 +32243,7 @@ msgstr "Беззвучний дзвінок" msgid "Silent calls" msgstr "Беззвучні дзвінки" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Без звуку, коли пристрій захоплення не має власного аудіовходу" @@ -32655,8 +32754,8 @@ msgstr "Щось пішло не так." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Щось пішло не так. Спробуйте ще раз за мить." @@ -32750,7 +32849,7 @@ msgstr "Звуки" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Джерело" @@ -32859,7 +32958,7 @@ msgstr "Говоріть. Натисніть «Зупинити», коли за #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Динамік" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Створити груповий чат" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Розпочати відеодзвінок?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Розпочати голосовий дзвінок?" @@ -33092,6 +33191,16 @@ msgstr "Дата початку має бути раніше дати завер msgid "Start drag and drop" msgstr "Почати перетягування" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Розпочати в режимі P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Почати в режимі P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Розпочати згорнутим" msgid "Start over" msgstr "Почати спочатку" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Розпочати P2P виклик" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Почніть пошук з <0>{peoplePrefix} <1>{textChannelPrefix msgid "Start speaking" msgstr "Почати озвучення" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Розпочати стандартний виклик" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Панель стану" msgid "Status:" msgstr "Статус:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Залишитися" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Зупинити" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Припинити бути каналом оголошень?" @@ -33921,6 +34045,11 @@ msgstr "Перейти до попередньої спільноти або п msgid "Switch to single column" msgstr "Перейти до одного стовпця" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Перейти до звичайного дзвінка" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Відкріплена група" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Відкріплений груповий чат" @@ -37727,6 +37856,11 @@ msgstr "Непідтверджена електронна пошта" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "До {DISCOVERY_MAX_TAGS} тегів допомагають людям знаходити вашу спільноту. Вони відображаються в пошуку Discovery." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "До #maxParticipants{maxParticipants, plural, one {особи} other {осіб}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Завантаження емоджі" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Надсилання відео:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Налаштування відео" @@ -39570,7 +39704,7 @@ msgstr "Запис голосових повідомлень не підтрим #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Голосовий регіон" @@ -39582,7 +39716,7 @@ msgstr "Панель відновлення голосового сеансу" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Налаштування голосу" diff --git a/fluxer_app/src/features/i18n/locales/vi/messages.po b/fluxer_app/src/features/i18n/locales/vi/messages.po index 56892fa58..71656e849 100644 --- a/fluxer_app/src/features/i18n/locales/vi/messages.po +++ b/fluxer_app/src/features/i18n/locales/vi/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# tin nhắn bị chặn} other {# tin nhắn bị chặn}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# kênh khác đang theo dõi} other {# kênh khác đang theo dõi}} kênh này. Chuyển đổi thành kênh văn bản sẽ xóa những người theo dõi đó." @@ -2720,6 +2720,11 @@ msgstr "Một biểu tượng cảm xúc mặc định. Bạn có thể sử d msgid "A description is required." msgstr "Cần có mô tả." +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "Không thể kết nối trực tiếp. Bạn không thể nghe hoặc nhìn thấy nhau." + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "một bản nhúng đa phương tiện hoặc bản xem trước liên k msgid "A separate profile for each community" msgstr "Một hồ sơ riêng cho mỗi cộng đồng" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "Cuộc gọi thông thường sẽ đi qua máy chủ và hoạt động với mọi người." + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "Quản trị viên đã tạm dừng lời mời, bạn không thể tha #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "Bất kỳ ai cũng có thể yêu cầu tài khoản, nhưng quản tr msgid "Anyone in my communities" msgstr "Bất kỳ ai trong cộng đồng của tôi" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "Những ai không thể kết nối trực tiếp sẽ không tham gia được cho đến khi có người chuyển cuộc gọi sang cuộc gọi tiêu chuẩn." + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "Bất kỳ ai tìm thấy bạn đều có thể gửi yêu cầu. Tắt tùy chọn này để chọn các nhóm bên dưới." @@ -5112,6 +5127,16 @@ msgstr "Âm thanh" msgid "Audio & video" msgstr "Âm thanh & video" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "Âm thanh và video đi trực tiếp giữa những người tham gia, tối đa {maxParticipants, plural, one {# người} other {# người}}. Mọi người đồng ý trước khi tham gia và có thể thấy địa chỉ IP của nhau." + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "Âm thanh và video đi trực tiếp giữa những người tham gia." + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "Gợi ý tự động hoàn tất: {label}, {selectedOptionPosition} tr #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "Tự động" @@ -6936,15 +6961,15 @@ msgstr "Không thể xóa tài khoản" msgid "Capture" msgstr "Thu âm thanh" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "Thu âm thanh ứng dụng" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "Thu âm thanh từ máy tính" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "Thu âm thanh thiết bị" @@ -7017,7 +7042,7 @@ msgstr "Đã sao chép ID danh mục" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "Tên danh mục" @@ -7554,7 +7579,7 @@ msgstr "Các mục chi tiết kênh" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "Đã sao chép ID kênh" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "liên kết kênh" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "Đã sao chép liên kết kênh" @@ -7604,7 +7629,7 @@ msgstr "Quản lý kênh" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "Kênh đã đồng bộ với danh mục mẹ" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "Chủ đề kênh quá dài." @@ -7668,8 +7693,8 @@ msgstr "Chọn loại kênh" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "Đã cập nhật kênh" @@ -9421,7 +9446,7 @@ msgstr "Xác nhận trước khi vào kênh thoại" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "Xác nhận trước khi bắt đầu cuộc gọi" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "Cuộc trò chuyện" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "Chuyển đổi" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "Đã chuyển kênh thành kênh thông báo" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "Chuyển đổi kênh này thành kênh văn bản sẽ xóa mọi kênh theo dõi nó." @@ -9949,7 +9974,7 @@ msgstr "Sao chép ID kênh" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "Sao chép liên kết kênh" @@ -10379,13 +10404,18 @@ msgstr "Không thể hoàn tất đăng nhập. Vui lòng thử lại." msgid "Couldn't connect to {domain}. Try again." msgstr "Không thể kết nối tới {domain}. Thử lại." +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "Không thể kết nối tới {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "Không thể kết nối với kênh thoại. Vui lòng thử lại." #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "Không thể chuyển đổi kênh này" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "Không thể tải bản cập nhật ứng dụng máy tính" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "Không thể nhân bản kênh này" @@ -10511,6 +10541,11 @@ msgstr "Không thể bỏ qua lời mời kết bạn. Vui lòng thử lại." msgid "Couldn't join community" msgstr "Không thể tham gia cộng đồng" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "Không thể tham gia cuộc gọi P2P. Thử lại." + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "Không thể mở cổng thanh toán" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "Không thể ghim cuộc trò chuyện này" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "Không thể chạy lệnh đó: {detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "Không thể lưu cài đặt kênh" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "Không thể bỏ theo dõi kênh này" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "Không thể bỏ ghim cuộc trò chuyện này" @@ -11374,7 +11409,7 @@ msgstr "Tùy chỉnh" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "Tùy chỉnh" @@ -11938,7 +11973,7 @@ msgstr "Mặc định" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "Xóa \"{stickerName}\"? Không thể hoàn tác." #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "Xóa {channelLabel}? Không thể hoàn tác." @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "Âm thanh thiết bị ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "Chỉ âm thanh thiết bị" @@ -12694,7 +12729,7 @@ msgstr "Chi tiết thiết bị" msgid "Device revoked" msgstr "Thiết bị đã bị thu hồi" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "Chọn thiết bị" @@ -12758,6 +12793,11 @@ msgstr "Kích cỡ & số lượng thành viên khác nhau" msgid "Dim strikethrough text" msgstr "Làm mờ văn bản gạch ngang" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "Kết nối trực tiếp. Phương tiện được mã hóa giữa những người tham gia." + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "Thanh bên DM" msgid "DNS TXT record" msgstr "Bản ghi DNS TXT" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "Không hỏi lại, luôn đồng ý" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "Tạo bản sao" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "Tạo bản sao kênh" @@ -13825,7 +13870,7 @@ msgstr "Chỉnh sửa danh mục" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "Chỉnh sửa kênh" @@ -15004,7 +15049,7 @@ msgstr "Phím Escape" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "Phím Escape thoát chế độ bàn phím" @@ -15047,6 +15092,11 @@ msgstr "Mọi cộng đồng theo dõi kênh này sẽ nhận được một b msgid "Everyone" msgstr "Mọi người" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "Mọi người trong cuộc gọi đều nhận được chất lượng luồng Plutonium miễn phí: độ phân giải nguồn lên tới 4K và 60 fps." + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "Đã cấp {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "Cấp tất cả quyền và bỏ qua các hạn chế kênh. Rất nhạy cảm." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "Chọn chi tiết" @@ -17417,7 +17467,7 @@ msgstr "Tiếng Hy Lạp" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "Chế độ xem lưới" @@ -17909,7 +17959,7 @@ msgstr "Ẩn người tham gia" msgid "Hide password" msgstr "Ẩn mật khẩu" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "Ẩn hình thu nhỏ xem trước" @@ -18322,11 +18372,11 @@ msgstr "Bỏ qua" msgid "Ignore friend request" msgstr "Bỏ qua lời mời kết bạn" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "Bỏ qua thiết bị phần cứng" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "Bỏ qua phương tiện đầu vào" @@ -18357,7 +18407,7 @@ msgstr "Bỏ qua lời mời kết bạn từ {displayName}?" msgid "Ignore this channel's slowmode cooldown." msgstr "Bỏ qua thời gian chờ chế độ chậm của kênh này." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "Bỏ qua thiết bị ảo" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "Thiết bị đầu vào" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "Mức đầu vào" @@ -18865,7 +18915,7 @@ msgstr "Giám sát đầu vào" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "Cấu hình đầu vào" @@ -19431,6 +19481,12 @@ msgstr "Độ trễ biến động:" msgid "Join" msgstr "Tham gia" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "Tham gia" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "Tham gia cộng đồng" msgid "Join a community to unlock stickers." msgstr "Tham gia một cộng đồng để mở khóa nhãn dán." +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "Tham gia cuộc gọi P2P?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "Cách tham gia" msgid "Join on this device" msgstr "Tham gia trên thiết bị này" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "Tham gia cuộc gọi P2P mà không hỏi" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "Rời khỏi {threadName}? Đây là cuộc trò chuyện riêng tư, n msgid "Leave blank for a random username" msgstr "Để trống nếu muốn tên người dùng ngẫu nhiên" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "Rời cuộc gọi" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "Cho phép CSS tùy chỉnh hiển thị các vùng cửa sổ trong suốt sau khi khởi động lại ứng dụng trên máy tính." #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "Cho phép các cộng đồng khác theo dõi kênh này và nhận bản sao những gì bạn đăng." @@ -20729,6 +20797,11 @@ msgstr "Louisiana" msgid "Low" msgstr "Thấp" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "Độ trễ thấp hơn." + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "Michigan" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "Minnesota" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "Phản chiếu camera" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "Nhãn dán tuyệt vời của tôi" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "Danh mục của tôi" @@ -24444,7 +24517,7 @@ msgstr "Chỉ tiếp tục nếu bạn đã bắt đầu đăng nhập này trê msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "Chỉ chấp nhận thẻ tín dụng. Không thể dùng thẻ ghi nợ và thẻ trả trước để kiểm tra nội dung người lớn." -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "Chỉ loa mặc định" @@ -24522,7 +24595,7 @@ msgstr "Chỉ những thành viên và vai trò được chọn mới có thể msgid "Only show message previews for DMs with unread messages" msgstr "Chỉ hiện bản xem trước tin nhắn cho các tin nhắn trực tiếp chưa đọc" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "Chỉ loa" @@ -24759,7 +24832,7 @@ msgstr "Mở chi tiết kênh {channelName}" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "Mở trò chuyện" @@ -24840,7 +24913,7 @@ msgstr "Mở biểu tượng cảm xúc trong trình duyệt" msgid "Open emoji picker" msgstr "Mở bảng chọn biểu tượng cảm xúc" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "Mở bộ thu sự kiện" @@ -25280,6 +25353,16 @@ msgstr "Các luồng hoạt động khác" msgid "Other devices" msgstr "Thiết bị khác" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "Những người tham gia khác có thể thấy địa chỉ IP của bạn." + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "Những người tham gia khác trong cuộc gọi P2P có thể thấy địa chỉ IP của bạn." + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "Không thể chuyển quyền sở hữu cho <0>{username} vào lúc msgid "Ownership transferred" msgstr "Đã chuyển quyền sở hữu" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "Cuộc gọi P2P hiện không khả dụng." + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "Chỉ gọi P2P" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "Đã ghim nhóm" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "Đã ghim tin nhắn nhóm" @@ -26536,7 +26635,7 @@ msgstr "Nút xem trước" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "Xem trước camera" @@ -27107,7 +27206,7 @@ msgstr "Phím nhấn để nói" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "Nhấn để nói" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "Rút gọn tin nhắn và thử lại." #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "Rút gọn chủ đề và thử lại." @@ -31488,7 +31587,7 @@ msgstr "Hiển thị điều khiển âm lượng kết nối" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "Hiện phím tắt menu ngữ cảnh" @@ -31573,7 +31672,7 @@ msgstr "Hiển thị ít hiệu ứng âm thanh hơn" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "Hiển thị vòng tiêu điểm trên vùng nhập văn bản trò chuyện" @@ -31701,7 +31800,7 @@ msgstr "Hiện các kênh đã tắt tiếng" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "Hiện camera của tôi" @@ -31742,7 +31841,7 @@ msgstr "Hiển thị bài viết mới dưới dạng chưa đọc" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "Hiện người tham gia không bật video" @@ -32144,7 +32243,7 @@ msgstr "Cuộc gọi im lặng" msgid "Silent calls" msgstr "Cuộc gọi im lặng" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "Im lặng khi thiết bị ghi âm không có đầu vào âm thanh riêng" @@ -32655,8 +32754,8 @@ msgstr "Đã xảy ra lỗi." #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "Đã xảy ra lỗi. Vui lòng thử lại sau giây lát." @@ -32750,7 +32849,7 @@ msgstr "Âm thanh" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "Nguồn" @@ -32859,7 +32958,7 @@ msgstr "Bắt đầu nói. Nhấn Dừng khi bạn hoàn tất — bạn có th #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "Loa" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "Tạo tin nhắn nhóm" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "Bắt đầu cuộc gọi video?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "Bắt đầu cuộc gọi thoại?" @@ -33092,6 +33191,16 @@ msgstr "Ngày bắt đầu phải sớm hơn ngày kết thúc." msgid "Start drag and drop" msgstr "Bắt đầu kéo và thả" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "Bắt đầu ở chế độ P2P" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "Bắt đầu ở chế độ P2P?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "Bắt đầu thu nhỏ" msgid "Start over" msgstr "Bắt đầu lại" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "Bắt đầu cuộc gọi P2P" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "Bắt đầu tìm kiếm bằng <0>{peoplePrefix} <1>{textChannelPre msgid "Start speaking" msgstr "Bắt đầu đọc" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "Bắt đầu cuộc gọi tiêu chuẩn" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "Bảng trạng thái" msgid "Status:" msgstr "Trạng thái:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "Tiếp tục" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "Dừng" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "Dừng làm kênh thông báo?" @@ -33921,6 +34045,11 @@ msgstr "Chuyển sang cộng đồng hoặc tin nhắn trực tiếp trước đ msgid "Switch to single column" msgstr "Chuyển sang chế độ một cột" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "Chuyển sang cuộc gọi thông thường" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "Đã bỏ ghim nhóm" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "Đã bỏ ghim tin nhắn nhóm" @@ -37727,6 +37856,11 @@ msgstr "Email chưa xác minh" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "Tối đa {DISCOVERY_MAX_TAGS} thẻ giúp mọi người tìm thấy cộng đồng của bạn. Các thẻ này sẽ hiển thị trong tìm kiếm Khám phá." +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "Tối đa {maxParticipants, plural, one {# người} other {# người}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "Đang tải biểu tượng cảm xúc lên" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "Gửi video:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "Cài đặt video" @@ -39570,7 +39704,7 @@ msgstr "Trình duyệt này không hỗ trợ ghi âm giọng nói." #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "Vùng thoại" @@ -39582,7 +39716,7 @@ msgstr "Thanh thông báo khôi phục phiên thoại" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "Cài đặt thoại" diff --git a/fluxer_app/src/features/i18n/locales/zh-CN/messages.po b/fluxer_app/src/features/i18n/locales/zh-CN/messages.po index 9d6bef78b..1f45785c5 100644 --- a/fluxer_app/src/features/i18n/locales/zh-CN/messages.po +++ b/fluxer_app/src/features/i18n/locales/zh-CN/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, other {# 条被屏蔽的消息}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# 个频道关注此频道} other {# 个频道关注此频道}}。将其转换为文本频道将移除这些关注。" @@ -2720,6 +2720,11 @@ msgstr "一个默认表情。您可以在 {productName} 的任何地方使用它 msgid "A description is required." msgstr "需要填写描述。" +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "无法建立直接连接。你们互相听不到也看不到对方。" + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "富媒体嵌入或链接预览(非上传文件)" msgid "A separate profile for each community" msgstr "每个社群的独立个人资料" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "普通通话会经过服务器,并且适用于所有人。" + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "管理员已暂停邀请,你暂时无法加入。" #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "任何人都可以申请账号,但需要管理员批准。" msgid "Anyone in my communities" msgstr "我社区里的任何人" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "无法直接连接的任何人,都必须等到有人将通话切换为标准通话后才能加入。" + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "找到你的人都可以发送请求。关闭此选项可选择下方的群组。" @@ -5112,6 +5127,16 @@ msgstr "音频" msgid "Audio & video" msgstr "音频和视频" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "音频和视频直接在参与者之间传输,最多支持 {maxParticipants, plural, one {# 位参与者} other {# 位参与者}}。加入前大家会互相同意,并且可以看到彼此的 IP 地址。" + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "音频和视频直接在参与者之间传输。" + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "自动补全建议:{label},第 {selectedOptionPosition} 项,共 {r #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "自动" @@ -6936,15 +6961,15 @@ msgstr "无法删除账号" msgid "Capture" msgstr "捕获" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "捕获应用音频" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "捕获桌面音频" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "捕获设备音频" @@ -7017,7 +7042,7 @@ msgstr "类别 ID 已复制" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "类别名称" @@ -7554,7 +7579,7 @@ msgstr "频道详情版块" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "频道 ID 已复制" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "频道链接" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "频道链接已复制" @@ -7604,7 +7629,7 @@ msgstr "频道管理" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "频道已与父类别同步" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "频道话题过长。" @@ -7668,8 +7693,8 @@ msgstr "选择频道类型" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "频道已更新" @@ -9421,7 +9446,7 @@ msgstr "加入语音频道前确认" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "发起通话前确认" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "对话" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "转换" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "将频道转换为公告频道" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "将此频道转换为文本频道将移除所有关注它的频道。" @@ -9949,7 +9974,7 @@ msgstr "复制频道 ID" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "复制频道链接" @@ -10379,13 +10404,18 @@ msgstr "无法完成登录。请重试。" msgid "Couldn't connect to {domain}. Try again." msgstr "无法连接到 {domain}。请重试。" +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "无法连接到{names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "无法连接到语音。请重试。" #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "无法转换此频道" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "无法下载桌面版更新" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "无法复制此频道" @@ -10511,6 +10541,11 @@ msgstr "无法忽略好友请求。请重试。" msgid "Couldn't join community" msgstr "无法加入社区" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "无法加入点对点通话。请重试。" + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "无法打开账单门户" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "无法置顶此对话" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "无法运行该命令:{detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "无法保存频道设置" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "无法取消关注此频道" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "无法取消置顶此对话" @@ -11374,7 +11409,7 @@ msgstr "自定义" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "自定义" @@ -11938,7 +11973,7 @@ msgstr "默认" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "删除“{stickerName}”?此操作无法撤销。" #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "删除“{channelLabel}”?此操作无法撤销。" @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "设备音频({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "仅设备音频" @@ -12694,7 +12729,7 @@ msgstr "设备详情" msgid "Device revoked" msgstr "设备已撤销" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "选择设备" @@ -12758,6 +12793,11 @@ msgstr "不同尺寸和成员数量" msgid "Dim strikethrough text" msgstr "调暗删除线文本" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "直接连接。媒体在参与者之间加密。" + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "私信侧边栏" msgid "DNS TXT record" msgstr "DNS TXT 记录" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "不再询问,始终同意" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "复制" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "复制频道" @@ -13825,7 +13870,7 @@ msgstr "编辑类别" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "编辑频道" @@ -15004,7 +15049,7 @@ msgstr "Esc 键" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "按 Esc 键退出键盘模式" @@ -15047,6 +15092,11 @@ msgstr "关注此频道的每个社群都会收到一份副本。如果您稍后 msgid "Everyone" msgstr "所有人" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "通话中的每个人都可以免费获得 Plutonium 流媒体质量:最高 4K 源分辨率和 60 fps。" + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "授予了 {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "授予所有权限并绕过频道限制。高度敏感。" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "精细选择" @@ -17417,7 +17467,7 @@ msgstr "希腊语" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "网格视图" @@ -17909,7 +17959,7 @@ msgstr "隐藏参与者" msgid "Hide password" msgstr "隐藏密码" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "隐藏预览缩略图" @@ -18322,11 +18372,11 @@ msgstr "忽略" msgid "Ignore friend request" msgstr "忽略好友请求" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "忽略硬件设备" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "忽略输入媒体" @@ -18357,7 +18407,7 @@ msgstr "忽略来自 {displayName} 的好友请求?" msgid "Ignore this channel's slowmode cooldown." msgstr "忽略此频道的慢速模式冷却时间。" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "忽略虚拟设备" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "输入设备" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "输入电平" @@ -18865,7 +18915,7 @@ msgstr "输入监控" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "输入配置" @@ -19431,6 +19481,12 @@ msgstr "抖动:" msgid "Join" msgstr "加入" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "加入" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "加入社区" msgid "Join a community to unlock stickers." msgstr "加入社区即可解锁贴纸。" +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "加入 P2P 通话?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "加入方式" msgid "Join on this device" msgstr "在此设备上加入" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "加入 P2P 通话,无需询问" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "离开 {threadName}?这是一个私密对话,除非有人再次添 msgid "Leave blank for a random username" msgstr "留空则随机生成用户名" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "离开通话" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "重启桌面应用后,让自定义 CSS 显示透明窗口区域。" #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "让其他社区关注此频道,并获取您发布内容的副本。" @@ -20729,6 +20797,11 @@ msgstr "路易斯安那州" msgid "Low" msgstr "低" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "低延迟。" + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "密歇根州" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "明尼苏达州" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "镜像摄像头" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "我的超赞贴纸" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "我的类别" @@ -24444,7 +24517,7 @@ msgstr "仅在你已在自己控制的设备上开始此登录时继续。" msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "仅接受信用卡。借记卡和预付卡不能用于成人内容验证。" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "仅默认扬声器" @@ -24522,7 +24595,7 @@ msgstr "仅选定的成员和角色可以查看此频道。" msgid "Only show message previews for DMs with unread messages" msgstr "仅为有未读消息的私信显示消息预览" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "仅扬声器" @@ -24759,7 +24832,7 @@ msgstr "打开“{channelName}”的频道详情" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "打开聊天" @@ -24840,7 +24913,7 @@ msgstr "在浏览器中打开表情" msgid "Open emoji picker" msgstr "打开表情选择器" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "打开事件接收器" @@ -25280,6 +25353,16 @@ msgstr "其他活跃的频道" msgid "Other devices" msgstr "其他设备" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "其他参与者可以看到您的 IP 地址。" + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "P2P 通话中的其他参与者可以看到您的 IP 地址。" + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "暂时无法将所有权转让给<0>{username}。" msgid "Ownership transferred" msgstr "所有权已转让" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "目前无法发起点对点通话。" + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "仅 P2P 呼叫" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "已置顶群聊" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "已置顶群聊" @@ -26536,7 +26635,7 @@ msgstr "预览按钮" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "预览摄像头" @@ -27107,7 +27206,7 @@ msgstr "按住说话按键" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "按住说话" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "请缩短消息后重试。" #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "请缩短话题后重试。" @@ -31488,7 +31587,7 @@ msgstr "显示连接音量控件" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "显示上下文菜单快捷键" @@ -31573,7 +31672,7 @@ msgstr "显示更少的音效" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "在聊天输入框中显示焦点环" @@ -31701,7 +31800,7 @@ msgstr "显示已静音频道" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "显示我的摄像头" @@ -31742,7 +31841,7 @@ msgstr "将新帖子显示为未读" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "显示非视频参与者" @@ -32144,7 +32243,7 @@ msgstr "静音通话" msgid "Silent calls" msgstr "静音来电" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "当捕获设备没有自己的音频输入时保持静默" @@ -32655,8 +32754,8 @@ msgstr "出错了。" #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "出错了。请稍后重试。" @@ -32750,7 +32849,7 @@ msgstr "声音" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "原画" @@ -32859,7 +32958,7 @@ msgstr "现在开始说话。完成后点击停止,之后可以修剪。" #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "扬声器" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "发起群聊" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "发起视频通话?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "发起语音通话?" @@ -33092,6 +33191,16 @@ msgstr "开始日期必须早于结束日期。" msgid "Start drag and drop" msgstr "开始拖放" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "以 P2P 模式开始" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "以 P2P 模式开始?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "最小化启动" msgid "Start over" msgstr "重新开始" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "发起点对点通话" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "搜索时以 <0>{peoplePrefix}、<1>{textChannelPrefix}、<2>{vo msgid "Start speaking" msgstr "开始朗读" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "发起标准通话" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "状态面板" msgid "Status:" msgstr "状态:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "保留" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "停止" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "停止发送公告?" @@ -33921,6 +34045,11 @@ msgstr "切换到上一个社区或私信" msgid "Switch to single column" msgstr "切换为单列" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "切换到标准通话" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "已取消置顶群聊" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "已取消置顶群聊" @@ -37727,6 +37856,11 @@ msgstr "邮箱未验证" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "最多可添加 {DISCOVERY_MAX_TAGS} 个标签,帮助大家发现你的社区。这些标签会显示在社区发现的搜索结果中。" +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "最多 {maxParticipants, plural, one {# 人} other {# 人}}。" + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "正在上传表情" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "视频发送:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "视频设置" @@ -39570,7 +39704,7 @@ msgstr "此浏览器不支持语音录制。" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "语音地区" @@ -39582,7 +39716,7 @@ msgstr "语音会话恢复提示栏" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "语音设置" diff --git a/fluxer_app/src/features/i18n/locales/zh-TW/messages.po b/fluxer_app/src/features/i18n/locales/zh-TW/messages.po index 533e2dc73..71aafdd82 100644 --- a/fluxer_app/src/features/i18n/locales/zh-TW/messages.po +++ b/fluxer_app/src/features/i18n/locales/zh-TW/messages.po @@ -1114,7 +1114,7 @@ msgid "{count, plural, one {# blocked message} other {# blocked messages}}" msgstr "{count, plural, one {# 則被封鎖的訊息} other {# 則被封鎖的訊息}}" #. Body of the confirmation shown before an announcement channel is converted back into a text channel. {count} is the number of channels, in this or other communities, that follow it and get copies of its published messages. Those channels stop getting copies after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:112 msgid "{count, plural, one {# channel follows} other {# channels follow}} this channel. Converting it to a text channel removes those follows." msgstr "{count, plural, one {# 個頻道正在追蹤} other {# 個頻道正在追蹤}}此頻道。轉換為文字頻道將會移除這些追蹤。" @@ -2720,6 +2720,11 @@ msgstr "預設表情符號。您可以在 {productName} 的任何地方使用它 msgid "A description is required." msgstr "必須填寫說明。" +#. Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:420 +msgid "A direct connection couldn't be made. You can't hear or see each other." +msgstr "無法建立直接連線。你們無法互相聽到或看到對方。" + #. Duration label component for a very short elapsed call. #: src/features/channel/components/CallMessage.tsx:63 msgid "a few seconds" @@ -2794,6 +2799,11 @@ msgstr "豐富的嵌入內容或連結預覽(非上傳檔案)" msgid "A separate profile for each community" msgstr "每個社群的獨立個人檔案" +#. Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:424 +msgid "A standard call goes through a server and works for everyone." +msgstr "一般通話會透過伺服器進行,而且適用於所有人。" + #. Description for has:sticker. Sentence case, no trailing punctuation. #: src/features/search/utils/SearchUtils.ts:51 msgid "a sticker" @@ -3676,7 +3686,7 @@ msgstr "管理員已暫停邀請,您目前無法加入。" #. Collapsed-by-default group at the end of the community overview tab holding less common settings. #. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats. #. User settings tab for power-user and experimental application preferences. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:519 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:546 #: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28 #: src/features/user/components/settings_utils/SettingsConstants.tsx:138 #: src/features/voice/components/voice_connection_status/VoiceDetailsPopout.tsx:60 @@ -4628,6 +4638,11 @@ msgstr "任何人都可以申請帳號,但必須由管理員核准。" msgid "Anyone in my communities" msgstr "我社群中的任何人" +#. Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:372 +msgid "Anyone who cannot connect directly cannot take part until someone switches the call to a standard call." +msgstr "若有人無法直接連線,就無法參與,直到有人將通話切換為標準通話為止。" + #: src/features/user/components/modals/tabs/privacy_safety_tab/ConnectionsTab.tsx:285 msgid "Anyone who finds you can send a request. Turn this off to pick groups below." msgstr "任何人找到你都可以傳送要求。關閉此選項即可選擇下方的群組。" @@ -5112,6 +5127,16 @@ msgstr "音訊" msgid "Audio & video" msgstr "音訊與視訊" +#. Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:451 +msgid "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address." +msgstr "音訊和視訊直接在參與者之間傳輸,最多支援 {maxParticipants, plural, one {# 人} other {# 人}}。所有人加入前都會同意,且能看到彼此的 IP 位址。" + +#. Lead sentence of the P2P call dialog explaining peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:350 +msgid "Audio and video go directly between participants." +msgstr "音訊和視訊會直接在參與者之間傳送。" + #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:78 msgid "Audio boost" @@ -5419,7 +5444,7 @@ msgstr "自動完成建議:{label},第 {selectedOptionPosition} 項,共 {r #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:76 #: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:97 #: src/features/user/components/settings_utils/section_registry/NotificationsSections.ts:264 -#: src/features/voice/components/VoiceSettingsMenus.tsx:93 +#: src/features/voice/components/VoiceSettingsMenus.tsx:97 msgid "Automatic" msgstr "自動" @@ -6936,15 +6961,15 @@ msgstr "無法刪除帳號" msgid "Capture" msgstr "擷取" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:813 msgid "Capture app audio" msgstr "擷取應用程式音訊" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:814 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:815 msgid "Capture desktop audio" msgstr "擷取桌面音訊" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:811 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:812 msgid "Capture device audio" msgstr "擷取裝置音訊" @@ -7017,7 +7042,7 @@ msgstr "已複製類別 ID" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Form field label for the name of a favorites category. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:77 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 #: src/features/expressions/components/modals/CreateFavoriteCategoryModal.tsx:20 msgid "Category name" msgstr "類別名稱" @@ -7554,7 +7579,7 @@ msgstr "頻道詳細資訊區塊" #. Toast confirming the channel ID was copied to the clipboard. #. Toast confirming the channel ID was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:114 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:120 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:37 msgid "Channel ID copied" msgstr "頻道 ID 已複製" @@ -7572,7 +7597,7 @@ msgid "channel link" msgstr "頻道連結" #. Toast confirming the channel link was copied to the clipboard. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:106 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:112 msgid "Channel link copied" msgstr "頻道連結已複製" @@ -7604,7 +7629,7 @@ msgstr "頻道管理" #. Label of the name field in the channel create modal. #. Label of the name input in forum channel settings. #. Short label in the channel duplicate modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:82 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 #: src/features/channel/components/modals/ChannelCreateModal.tsx:47 #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:32 #: src/features/forum/utils/ForumMessageDescriptors.ts:331 @@ -7651,7 +7676,7 @@ msgid "Channel synced with parent category" msgstr "頻道已與父類別同步" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:68 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 msgid "Channel topic is too long." msgstr "頻道主題過長。" @@ -7668,8 +7693,8 @@ msgstr "選擇頻道類型" #. Audit log entry label. Past-tense action describing that a channel was edited. #. Toast shown after forum channel settings are saved. #: src/features/app/config/AuditLogConstants.ts:14 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:287 -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:329 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:295 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:338 #: src/features/forum/utils/ForumMessageDescriptors.ts:335 msgid "Channel updated" msgstr "頻道已更新" @@ -9421,7 +9446,7 @@ msgstr "在加入語音頻道前確認" #. Settings search entry label. Names the settings search entry in the settings UI. #. Short label in the keyboard tab. Keep it concise. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:23 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:28 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:17 msgid "Confirm before starting calls" msgstr "通話前確認" @@ -9822,7 +9847,7 @@ msgid "Conversations" msgstr "對話" #. Button in the confirmation that converts an announcement channel back into a text channel. Keep it short. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:123 msgid "Convert" msgstr "轉換" @@ -9842,7 +9867,7 @@ msgid "Converted the channel to an announcement channel" msgstr "將頻道轉換為公告頻道" #. Body of the confirmation shown before an announcement channel is converted back into a text channel, used when the number of following channels could not be loaded. Channels that follow it get copies of its published messages and stop getting them after the conversion. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:113 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:118 msgid "Converting this channel to a text channel removes every channel that follows it." msgstr "將此頻道轉換為文字頻道將會移除所有追蹤它的頻道。" @@ -9949,7 +9974,7 @@ msgstr "複製頻道 ID" #. Action that copies a deep link to the selected channel. #. Keyboard shortcut action label shown in the shortcuts list and keybind editor. Keep short, sentence case. #: src/features/input/state/input_keybind/shared.ts:213 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:138 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:144 msgid "Copy channel link" msgstr "複製頻道連結" @@ -10379,13 +10404,18 @@ msgstr "無法完成登入。請再試一次。" msgid "Couldn't connect to {domain}. Try again." msgstr "無法連線至 {domain}。請再試一次。" +#. Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:415 +msgid "Couldn't connect to {names}" +msgstr "無法連線至 {names}" + #. Toast shown when joining a voice channel fails because the connection timed out or the transport dropped. #: src/features/voice/engine/media_engine_facade/shared.ts:35 msgid "Couldn't connect to voice. Please try again." msgstr "無法連線至語音。請再試一次。" #. Title of the error dialog shown when converting a channel between a text channel and an announcement channel failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:122 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 msgid "Couldn't convert this channel" msgstr "無法轉換此頻道" @@ -10482,7 +10512,7 @@ msgid "Couldn't download the desktop update" msgstr "無法下載桌面版更新" #. Title of the error modal shown when duplicating a channel fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:146 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:152 msgid "Couldn't duplicate this channel" msgstr "無法複製此頻道" @@ -10511,6 +10541,11 @@ msgstr "無法忽略好友邀請。請再試一次。" msgid "Couldn't join community" msgstr "無法加入社群" +#. Error shown when the server refused to join a peer-to-peer call even though the user agreed to it. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:442 +msgid "Couldn't join the P2P call. Try again." +msgstr "無法加入點對點通話。請再試一次。" + #. Title of the generic fallback error modal shown when joining a discovery community fails. #: src/features/discovery/commands/DiscoveryJoinCommands.tsx:14 msgid "Couldn't join this community" @@ -10601,7 +10636,7 @@ msgid "Couldn't open the billing portal" msgstr "無法開啟帳務入口網站" #. Title of the error modal shown when pinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:126 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:132 msgid "Couldn't pin this conversation" msgstr "無法釘選此對話" @@ -10708,7 +10743,7 @@ msgid "Couldn't run that command: {detail}" msgstr "無法執行該指令:{detail}" #. Title of the error dialog shown when saving the channel overview settings failed. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:127 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:132 msgid "Couldn't save channel settings" msgstr "無法儲存頻道設定" @@ -10805,7 +10840,7 @@ msgid "Couldn't unfollow this channel" msgstr "無法取消追蹤此頻道" #. Title of the error modal shown when unpinning a DM or group DM fails. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:130 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:136 msgid "Couldn't unpin this conversation" msgstr "無法取消釘選此對話" @@ -11374,7 +11409,7 @@ msgstr "自訂" #. Voice input processing profile where the user configures processing manually. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:91 #: src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx:42 -#: src/features/voice/components/VoiceSettingsMenus.tsx:70 +#: src/features/voice/components/VoiceSettingsMenus.tsx:74 msgctxt "voice-processing-profile" msgid "Custom" msgstr "自訂" @@ -11938,7 +11973,7 @@ msgstr "預設" #. Default device option label in the voice settings menu (alternate call site for the same concept as DEFAULT_DESCRIPTOR). #. Fallback device option shown before a microphone, speaker, or camera label is available. #: src/features/voice/components/modals/CameraPreviewModal.tsx:68 -#: src/features/voice/components/VoiceSettingsMenus.tsx:87 +#: src/features/voice/components/VoiceSettingsMenus.tsx:91 #: src/features/voice/utils/SettingsDeviceOptions.ts:11 msgctxt "device-option" msgid "Default" @@ -12098,7 +12133,7 @@ msgstr "要刪除「{stickerName}」嗎?此動作無法復原。" #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #. Confirm dialog body before deleting a channel. #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:103 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:110 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:116 msgid "Delete {channelLabel}? Can't be undone." msgstr "要刪除「{channelLabel}」嗎?此動作無法復原。" @@ -12680,7 +12715,7 @@ msgid "Device audio ({deviceLabel})" msgstr "裝置音訊 ({deviceLabel})" #. Screen-share audio summary on a video device share that publishes only the audio of the capture device itself. -#: src/features/voice/components/StreamSettingsMenuContent.tsx:868 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:869 #: src/features/voice/utils/ScreenShareAudioSummary.ts:50 msgid "Device audio only" msgstr "僅音訊裝置" @@ -12694,7 +12729,7 @@ msgstr "裝置詳細資訊" msgid "Device revoked" msgstr "裝置已撤銷" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:1006 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:1007 msgid "Device selection" msgstr "選擇裝置" @@ -12758,6 +12793,11 @@ msgstr "不同尺寸和成員數" msgid "Dim strikethrough text" msgstr "調暗刪除線文字" +#. Tooltip on the P2P indicator of a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:336 +msgid "Direct connection. Media is encrypted between participants." +msgstr "直接連線。媒體內容會在參與者之間加密。" + #. Voice input processing profile with raw microphone audio. #: src/features/voice/utils/VoiceMessageDescriptors.ts:199 msgid "Direct input" @@ -13282,6 +13322,11 @@ msgstr "私訊側邊欄" msgid "DNS TXT record" msgstr "DNS TXT 記錄" +#. Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:391 +msgid "Do not ask again, always agree" +msgstr "不再詢問,一律同意" + #. Short label in the app constants. #. Tray status menu item. User presence that suppresses notifications. #: src/features/app/constants/AppConstants.ts:33 @@ -13691,7 +13736,7 @@ msgstr "複製" #. Action that creates a copy of the selected channel. #. Short label in the channel duplicate modal. Keep it concise. #: src/features/channel/components/modals/ChannelDuplicateModal.tsx:28 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:142 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:148 msgid "Duplicate channel" msgstr "複製頻道" @@ -13825,7 +13870,7 @@ msgstr "編輯類別" #. Action that opens the edit-channel modal. #. Channel details bottom sheet label or action (channel type, settings, toast, confirmation, or notification option). #: src/features/channel/components/bottomsheets/channel_details_bottom_sheet/ChannelDetailsBottomSheetShared.ts:153 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:154 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:160 #: src/features/ui/action_menu/items/ChannelMenuItems.tsx:72 msgid "Edit channel" msgstr "編輯頻道" @@ -15004,7 +15049,7 @@ msgstr "Esc 鍵" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:15 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:20 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:197 msgid "Escape key exits keyboard mode" msgstr "按下 Esc 鍵可離開鍵盤模式" @@ -15047,6 +15092,11 @@ msgstr "所有追蹤此頻道的社群都會收到一份副本。如果您之後 msgid "Everyone" msgstr "所有人" +#. Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:358 +msgid "Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps." +msgstr "通話中的每個人都能免費享有 Plutonium 串流品質:最高可達 4K 原始解析度和 60 fps。" + #. Premium model option that gives everyone the highest limits. #: src/features/app/components/setup/SetupWizardSteps.tsx:428 msgid "Everyone is premium" @@ -17381,7 +17431,7 @@ msgstr "已授予 {permissions}" msgid "Grants all permissions and bypasses channel restrictions. Highly sensitive." msgstr "授予所有權限並繞過頻道限制。高度敏感。" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:993 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:994 msgid "Granular selection" msgstr "精細選取" @@ -17417,7 +17467,7 @@ msgstr "希臘文" #. Toggle label in the mobile video settings bottom sheet. Switches to grid layout. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:69 -#: src/features/voice/components/VoiceSettingsMenus.tsx:789 +#: src/features/voice/components/VoiceSettingsMenus.tsx:796 msgid "Grid view" msgstr "格狀顯示" @@ -17909,7 +17959,7 @@ msgstr "隱藏參與者" msgid "Hide password" msgstr "隱藏密碼" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:770 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:771 msgid "Hide preview thumbnail" msgstr "隱藏預覽縮圖" @@ -18322,11 +18372,11 @@ msgstr "忽略" msgid "Ignore friend request" msgstr "忽略好友邀請" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:983 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:984 msgid "Ignore hardware devices" msgstr "忽略硬體裝置" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:959 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:960 msgid "Ignore input media" msgstr "忽略輸入媒體" @@ -18357,7 +18407,7 @@ msgstr "要忽略來自 {displayName} 的好友邀請嗎?" msgid "Ignore this channel's slowmode cooldown." msgstr "忽略此頻道的慢速模式冷卻時間。" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:970 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:971 msgid "Ignore virtual" msgstr "忽略虛擬裝置" @@ -18853,7 +18903,7 @@ msgid "Input device" msgstr "輸入裝置" #. Read-only microphone input level shown in the voice device menu. -#: src/features/voice/components/VoiceSettingsMenus.tsx:102 +#: src/features/voice/components/VoiceSettingsMenus.tsx:106 msgid "Input level" msgstr "輸入音量強度" @@ -18865,7 +18915,7 @@ msgstr "輸入監控" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Voice settings menu label for microphone processing profile. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:134 -#: src/features/voice/components/VoiceSettingsMenus.tsx:75 +#: src/features/voice/components/VoiceSettingsMenus.tsx:79 msgid "Input profile" msgstr "輸入設定檔" @@ -19431,6 +19481,12 @@ msgstr "抖動:" msgid "Join" msgstr "加入" +#. Primary button in the dialog shown before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:386 +msgctxt "voice-p2p-consent" +msgid "Join" +msgstr "加入" + #. Accessible label of a button that joins a community. {communityName} is the community name. #. Nagbar button label that opens the official product community invite. communityName is the community name. #: src/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar.tsx:29 @@ -19455,6 +19511,11 @@ msgstr "加入社群" msgid "Join a community to unlock stickers." msgstr "加入社群即可解鎖貼圖。" +#. Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:345 +msgid "Join a P2P call?" +msgstr "加入 P2P 通話?" + #. Action label in the DM channel header pill that joins an ongoing call. #. Primary CTA button in the mobile direct-call lobby bottom sheet. Joins the active call. #: src/features/channel/components/channel_view/dm_channel_view/shared.ts:125 @@ -19493,6 +19554,11 @@ msgstr "加入方式" msgid "Join on this device" msgstr "在此裝置上加入" +#. Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:396 +msgid "Join P2P calls without asking" +msgstr "直接加入點對點通話,無需詢問" + #. Call-to-action button label on an in-progress call system message. #: src/features/channel/components/CallMessage.tsx:67 msgid "Join the call" @@ -20023,8 +20089,10 @@ msgstr "離開「{threadName}」嗎?這是私人討論串,除非有人再次 msgid "Leave blank for a random username" msgstr "留空以隨機產生使用者名稱" +#. Button in the peer-to-peer connection failure dialog that leaves the call. #. Destructive button in the mobile direct-call lobby bottom sheet. Disconnects from the call. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:92 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:434 msgid "Leave call" msgstr "離開通話" @@ -20201,7 +20269,7 @@ msgid "Lets custom CSS reveal transparent window regions after restarting the de msgstr "重新啟動桌面版應用程式後,讓自訂 CSS 顯示透明視窗區域。" #. Description under the Announcement channel switch in channel settings. Turning it on converts a text channel into an announcement channel. Publishing a message sends a copy of it to every channel in other communities that follows this one. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 msgid "Lets other communities follow this channel and get copies of what you publish." msgstr "讓其他社群追蹤此頻道,並取得您發佈內容的副本。" @@ -20729,6 +20797,11 @@ msgstr "路易斯安那州" msgid "Low" msgstr "低" +#. Bullet point in the P2P call dialog. A benefit of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:354 +msgid "Lower latency." +msgstr "延遲更低。" + #. Country name. EU member state in the illegal-content report country dropdown. #: src/features/moderation/components/report/OptionDescriptors.ts:92 msgid "Luxembourg" @@ -22003,7 +22076,7 @@ msgstr "密西根州" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/permissions/system/components/useMacPermissionControl.ts:10 #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:26 -#: src/features/voice/components/VoiceSettingsMenus.tsx:79 +#: src/features/voice/components/VoiceSettingsMenus.tsx:83 #: src/features/voice/utils/ScreenShareAudioSummary.ts:41 #: src/features/voice/utils/SettingsDeviceOptions.ts:16 msgid "Microphone" @@ -22101,7 +22174,7 @@ msgstr "明尼蘇達州" #. Switch label in the video tab for flipping the local camera preview horizontally. #: src/features/user/components/modals/tabs/UserVideoTab.tsx:101 #: src/features/voice/components/modals/CameraPreviewModal.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:114 +#: src/features/voice/components/VoiceSettingsMenus.tsx:118 msgid "Mirror camera" msgstr "鏡像相機" @@ -22823,7 +22896,7 @@ msgid "My awesome sticker" msgstr "我的超讚貼圖" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:87 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 msgid "My category" msgstr "我的類別" @@ -24444,7 +24517,7 @@ msgstr "請確認您是在自己控制的裝置上開始此登入流程。" msgid "Only credit cards are accepted. Debit and prepaid cards cannot be used for the mature content check." msgstr "僅接受信用卡。簽帳金融卡和預付卡無法用於成人內容檢查。" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:948 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:949 msgid "Only default speakers" msgstr "僅限預設揚聲器" @@ -24522,7 +24595,7 @@ msgstr "只有選定的成員和角色才能檢視此頻道。" msgid "Only show message previews for DMs with unread messages" msgstr "只顯示有未讀訊息的私訊預覽" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:938 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:939 msgid "Only speakers" msgstr "僅限揚聲器" @@ -24759,7 +24832,7 @@ msgstr "開啟「{channelName}」的頻道詳細資訊" #. Context-menu action that opens a voice channel's embedded chat view without connecting to voice. #. Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list. #: src/features/app/components/layout/ChannelItem.tsx:129 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:158 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:164 msgid "Open chat" msgstr "開啟聊天" @@ -24840,7 +24913,7 @@ msgstr "在瀏覽器中開啟表情符號" msgid "Open emoji picker" msgstr "開啟表情符號選取器" -#: src/features/voice/components/VoiceSettingsMenus.tsx:863 +#: src/features/voice/components/VoiceSettingsMenus.tsx:888 msgid "Open event sink" msgstr "開啟事件接收器" @@ -25280,6 +25353,16 @@ msgstr "其他活躍討論串" msgid "Other devices" msgstr "其他裝置" +#. Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:363 +msgid "Other participants can see your IP address." +msgstr "其他參與者可以看到你的 IP 位址。" + +#. Description under the switch that skips the confirmation dialog before joining a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:401 +msgid "Other participants in a P2P call can see your IP address." +msgstr "其他參與者在點對點通話中可以看到你的 IP 位址。" + #. Privacy note in profile timezone settings. Preserve {timezoneIdentifierExample}; it is inserted by code as an IANA timezone identifier example. productName is the app name. Users should understand that only the UTC offset is shown to other people. #: src/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings.tsx:49 msgid "Other people can only see your current UTC offset when you choose to share profile local time. They do not see your exact time zone identifier, such as {timezoneIdentifierExample}. {productName} stores that identifier only so the offset can update automatically when daylight saving time changes." @@ -25395,6 +25478,22 @@ msgstr "目前無法將擁有權轉移給 <0>{username}。" msgid "Ownership transferred" msgstr "已轉移擁有權" +# auto-i18n: reviewed unchanged +#. Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:331 +msgid "P2P" +msgstr "P2P" + +#. Error shown when the server refuses to start or join a peer-to-peer call. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:438 +msgid "P2P calls are unavailable right now." +msgstr "目前無法進行點對點通話。" + +#. Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:446 +msgid "P2P calls only" +msgstr "僅限點對點通話" + #. Voice stats field label (mobile lobby). Shows packet loss percentage. #. Voice stats field label (mobile voice lobby). Shows packet loss percentage. #: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:130 @@ -26096,7 +26195,7 @@ msgstr "已釘選群組" #. Toast confirming a group DM was pinned in the sidebar. #. Toast confirming a group DM was pinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:118 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:124 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:13 msgid "Pinned group DM" msgstr "已釘選群組私訊" @@ -26536,7 +26635,7 @@ msgstr "預覽按鈕" #. Voice menu action that opens a preview of the current user camera. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:58 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:61 -#: src/features/voice/components/VoiceSettingsMenus.tsx:617 +#: src/features/voice/components/VoiceSettingsMenus.tsx:621 msgid "Preview camera" msgstr "預覽相機" @@ -27107,7 +27206,7 @@ msgstr "按住說話按鍵" #. Switch label in the voice tab toggling push-to-talk mode. Keep it concise. #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:112 #: src/features/user/components/modals/tabs/UserVoiceTab.tsx:354 -#: src/features/voice/components/VoiceSettingsMenus.tsx:422 +#: src/features/voice/components/VoiceSettingsMenus.tsx:426 msgid "Push-to-talk" msgstr "按住說話" @@ -31388,7 +31487,7 @@ msgid "Shorten the message and try again." msgstr "請縮短訊息後再試一次。" #. Body of the error modal shown when the channel topic exceeds the maximum length. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:73 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:78 msgid "Shorten the topic and try again." msgstr "縮短主題後再試一次。" @@ -31488,7 +31587,7 @@ msgstr "顯示連線音量控制項" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:19 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:24 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:221 msgid "Show context menu shortcuts" msgstr "顯示右鍵選單的快速鍵" @@ -31573,7 +31672,7 @@ msgstr "顯示較少音效" #. Label in the keyboard tab. #. Settings search entry label. Names the settings search entry in the settings UI. -#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:11 +#: src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx:16 #: src/features/user/components/settings_utils/search_index/AccessibilityIndex.ts:245 msgid "Show focus ring on chat text area" msgstr "在聊天文字輸入區顯示焦點框" @@ -31701,7 +31800,7 @@ msgstr "顯示已靜音的頻道" #. Toggle label in the mobile video settings bottom sheet. Controls whether the local camera tile is visible. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:73 -#: src/features/voice/components/VoiceSettingsMenus.tsx:819 +#: src/features/voice/components/VoiceSettingsMenus.tsx:826 msgid "Show my own camera" msgstr "顯示我的相機" @@ -31742,7 +31841,7 @@ msgstr "將新貼文顯示為未讀" #. Voice display preference checkbox that controls whether participants without video are shown in the grid. #: src/features/ui/action_menu/items/voice_participant_menu_data/shared.ts:103 #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:77 -#: src/features/voice/components/VoiceSettingsMenus.tsx:833 +#: src/features/voice/components/VoiceSettingsMenus.tsx:840 msgid "Show non-video participants" msgstr "顯示非視訊參與者" @@ -32144,7 +32243,7 @@ msgstr "靜音來電" msgid "Silent calls" msgstr "靜音來電" -#: src/features/voice/components/StreamSettingsMenuContent.tsx:874 +#: src/features/voice/components/StreamSettingsMenuContent.tsx:875 msgid "Silent when the capture device has no audio input of its own" msgstr "當擷取裝置沒有自己的音訊輸入時保持靜音" @@ -32655,8 +32754,8 @@ msgstr "發生錯誤。" #: src/features/emoji/components/emojis/EmojiListItem.tsx:129 #: src/features/theme/components/modals/ShareThemeModal.tsx:52 #: src/features/ui/action_menu/items/CallMenuItems.tsx:45 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:134 -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:150 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:140 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:156 msgid "Something went wrong. Please try again in a moment." msgstr "發生錯誤。請稍後再試一次。" @@ -32750,7 +32849,7 @@ msgstr "音效" #: src/features/user/components/modals/tabs/UserVideoTab.tsx:78 #: src/features/voice/components/modals/ScreenSharePickerModal.tsx:249 #: src/features/voice/components/StreamSettingsMenuContent.tsx:111 -#: src/features/voice/utils/VoiceMessageDescriptors.ts:332 +#: src/features/voice/utils/VoiceMessageDescriptors.ts:458 msgid "Source" msgstr "來源" @@ -32859,7 +32958,7 @@ msgstr "請開始說話。完成後請按「停止」,您之後可以再修剪 #. Fallback speaker option label when the operating system does not report a device name. #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/VoiceVideoSections.ts:46 -#: src/features/voice/components/VoiceSettingsMenus.tsx:83 +#: src/features/voice/components/VoiceSettingsMenus.tsx:87 #: src/features/voice/utils/SettingsDeviceOptions.ts:20 msgid "Speaker" msgstr "揚聲器" @@ -33057,12 +33156,12 @@ msgid "Start a group DM" msgstr "發起群組私訊" #. Confirm dialog body before starting a 1:1 video call. -#: src/features/voice/utils/CallUtils.tsx:17 +#: src/features/voice/utils/CallUtils.tsx:21 msgid "Start a video call?" msgstr "要開始視訊通話嗎?" #. Confirm dialog body before starting a 1:1 voice call. -#: src/features/voice/utils/CallUtils.tsx:21 +#: src/features/voice/utils/CallUtils.tsx:25 msgid "Start a voice call?" msgstr "要開始語音通話嗎?" @@ -33092,6 +33191,16 @@ msgstr "開始日期必須早於結束日期。" msgid "Start drag and drop" msgstr "開始拖放" +#. Primary button in the dialog offering to start a call in peer-to-peer mode. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:377 +msgid "Start in P2P mode" +msgstr "以 P2P 模式開始" + +#. Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:340 +msgid "Start in P2P mode?" +msgstr "以 P2P 模式開始?" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts:231 msgid "Start in the tray instead of opening the window when launched at login" @@ -33117,6 +33226,11 @@ msgstr "最小化啟動" msgid "Start over" msgstr "重新開始" +#. Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:410 +msgid "Start P2P call" +msgstr "開始點對點通話" + #. Button label shown before recording starts. #: src/features/voice/components/VoiceMessageComposerModal.tsx:69 msgid "Start recording" @@ -33131,6 +33245,11 @@ msgstr "搜尋時以 <0>{peoplePrefix}、<1>{textChannelPrefix}、<2>{vo msgid "Start speaking" msgstr "開始朗讀" +#. Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:381 +msgid "Start standard call" +msgstr "開始標準通話" + #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts:151 msgid "Start uploading attachments as soon as they are added to the message input" @@ -33202,6 +33321,11 @@ msgstr "狀態面板" msgid "Status:" msgstr "狀態:" +#. Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:429 +msgid "Stay" +msgstr "保留" + #. Empty state explanation in the thread browser. #: src/features/threads/utils/ThreadMessageDescriptors.ts:198 msgid "Stay focused on a conversation with a thread, a temporary text channel." @@ -33409,7 +33533,7 @@ msgid "Stop" msgstr "停止" #. Title of the confirmation shown when a member turns off the Announcement channel switch in channel settings, which converts the channel back into a normal text channel. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:102 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:107 msgid "Stop being an announcement channel?" msgstr "停止將其設為公告頻道?" @@ -33921,6 +34045,11 @@ msgstr "切換到上一個社群或私訊" msgid "Switch to single column" msgstr "切換成單欄顯示" +#. Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:405 +msgid "Switch to standard call" +msgstr "切換至一般通話" + #. Billing confirmation title for moving an active subscription down to the current price. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx:60 msgid "Switch to the new price?" @@ -37568,7 +37697,7 @@ msgstr "已取消釘選群組" #. Toast confirming a group DM was unpinned in the sidebar. #. Toast confirming a group DM was unpinned in the sidebar. -#: src/features/ui/action_menu/items/ChannelMenuData.tsx:122 +#: src/features/ui/action_menu/items/ChannelMenuData.tsx:128 #: src/features/ui/action_menu/items/dm_menu_data/shared.ts:25 msgid "Unpinned group DM" msgstr "已取消釘選群組私訊" @@ -37727,6 +37856,11 @@ msgstr "未驗證的電子郵件" msgid "Up to {DISCOVERY_MAX_TAGS} tags help people find your community. They show up in Discovery search." msgstr "最多可設定 {DISCOVERY_MAX_TAGS} 個標籤,讓大家更容易找到您的社群。這些標籤會顯示在探索搜尋結果中。" +#. Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4. +#: src/features/voice/utils/VoiceMessageDescriptors.ts:367 +msgid "Up to {maxParticipants, plural, one {# person} other {# people}}." +msgstr "最多 {maxParticipants, plural, one {# 位使用者} other {# 位使用者}}." + #. Plutonium page comparison value for paid video quality. Keep 4K and FPS as written. #: src/features/premium/components/plutonium_page/PlutoniumPageMessages.ts:235 msgid "Up to 4K at 60 FPS" @@ -38041,7 +38175,7 @@ msgstr "正在上傳表情符號" #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label for the URL input in the embed debugger. #. Short label in the channel create modal. Keep it concise. -#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:92 +#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:97 #: src/features/channel/components/modals/ChannelCreateModal.tsx:51 #: src/features/user/components/modals/tabs/EmbedDebuggerTab.tsx:34 #: src/features/user/components/settings_utils/search_index/EmbedDebuggerIndex.ts:19 @@ -39035,7 +39169,7 @@ msgstr "視訊傳送:" #. Menu action that opens video settings. #. Title of the mobile video settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:65 -#: src/features/voice/components/VoiceSettingsMenus.tsx:110 +#: src/features/voice/components/VoiceSettingsMenus.tsx:114 msgid "Video settings" msgstr "視訊設定" @@ -39570,7 +39704,7 @@ msgstr "您的瀏覽器不支援語音訊息錄製功能。" #. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate). #. Section header in the voice settings menu for the voice region picker. #: src/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect.tsx:24 -#: src/features/voice/components/VoiceSettingsMenus.tsx:98 +#: src/features/voice/components/VoiceSettingsMenus.tsx:102 msgid "Voice region" msgstr "語音區域" @@ -39582,7 +39716,7 @@ msgstr "語音連線還原提示列" #. Menu action that opens voice settings. #. Title of the mobile voice settings bottom sheet. #: src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx:53 -#: src/features/voice/components/VoiceSettingsMenus.tsx:106 +#: src/features/voice/components/VoiceSettingsMenus.tsx:110 msgid "Voice settings" msgstr "語音設定" diff --git a/fluxer_app/src/features/threads/state/ChannelThreads.test.ts b/fluxer_app/src/features/threads/state/ChannelThreads.test.ts index d8b7d8df9..c89f1ddb0 100644 --- a/fluxer_app/src/features/threads/state/ChannelThreads.test.ts +++ b/fluxer_app/src/features/threads/state/ChannelThreads.test.ts @@ -470,6 +470,7 @@ describe('control arm', () => { 'position', 'rate_limit_per_user', 'recipients', + 'rtc_p2p', 'rtc_region', 'topic', 'type', diff --git a/fluxer_app/src/features/ui/action_menu/items/ChannelMenuData.tsx b/fluxer_app/src/features/ui/action_menu/items/ChannelMenuData.tsx index 6b4078569..7568ed041 100644 --- a/fluxer_app/src/features/ui/action_menu/items/ChannelMenuData.tsx +++ b/fluxer_app/src/features/ui/action_menu/items/ChannelMenuData.tsx @@ -86,6 +86,7 @@ import { PinIcon, SendInvitesIcon, SettingsIcon, + VoiceCallIcon, } from '@app/features/ui/action_menu/ContextMenuIcons'; import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import {modal} from '@app/features/ui/commands/ModalCommands'; @@ -94,7 +95,12 @@ import * as ToastCommands from '@app/features/ui/commands/ToastCommands'; import type {MenuActionEvent, MenuGroupType, MenuItemType} from '@app/features/ui/menu_bottom_sheet/MenuBottomSheet'; import UserGuildSettings from '@app/features/user/state/UserGuildSettings'; import UserSettings from '@app/features/user/state/UserSettings'; +import {VoiceP2pConsentModal} from '@app/features/voice/components/alerts/VoiceP2pConsentModal'; +import MediaEngine from '@app/features/voice/engine/MediaEngineFacade'; import CompactVoiceCallHeight, {getGuildVoiceCallExpansionKey} from '@app/features/voice/state/CompactVoiceCallHeight'; +import VoiceP2pConsent from '@app/features/voice/state/VoiceP2pConsent'; +import VoiceP2pRollout from '@app/features/voice/state/VoiceP2pRollout'; +import {VOICE_P2P_START_CALL_DESCRIPTOR} from '@app/features/voice/utils/VoiceMessageDescriptors'; import {getMutedText} from '@app/lib/overlay/OverlayContextMenu'; import {ME} from '@fluxer/constants/src/AppConstants'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; @@ -182,6 +188,7 @@ interface ChannelMenuHandlers { handleOpenChannelLink: () => void; handleCopyLinkChannelUrl: () => Promise; handleOpenChat: () => void; + handleStartP2pCall: () => void; handleOpenMuteSheet: () => void; handleNotificationSettings: () => void; handleDuplicateChannel: (event?: MenuActionEvent) => void; @@ -212,6 +219,7 @@ interface ChannelMenuState { canManageChannels: boolean; canEditChannel: boolean; canInvite: boolean; + canStartP2pCall: boolean; canFollow: boolean; developerMode: boolean; isPinned: boolean; @@ -246,6 +254,12 @@ function getChannelMenuState(channel: Channel, guild: Guild | undefined): Channe }); const canEditChannel = canManageChannels || canUpdateRtcRegion; const canInvite = InviteUtils.canInviteToChannel(channel.id, channel.guildId); + const canStartP2pCall = + isVoiceChannel && + !channel.rtcP2p && + VoiceP2pRollout.enabled && + Object.keys(MediaEngine.getAllVoiceStatesInChannel(channel.guildId ?? ME, channel.id)).length === 0 && + Permission.can(Permissions.CONNECT, {channelId: channel.id, guildId: channel.guildId}); const canFollow = canFollowAnnouncementChannel(channel); const developerMode = UserSettings.developerMode; const isPinned = channel.isPinned; @@ -262,6 +276,7 @@ function getChannelMenuState(channel: Channel, guild: Guild | undefined): Channe canManageChannels, canEditChannel, canInvite, + canStartP2pCall, canFollow, developerMode, isPinned, @@ -353,6 +368,24 @@ export function useChannelMenuData( } onClose(); }, + handleStartP2pCall: () => { + const guildId = channel.guildId ?? null; + ModalCommands.pushAfterBottomSheetClose( + onClose, + modal(() => ( + { + VoiceP2pConsent.agree(channel.id); + void MediaEngine.connectToVoiceChannel(guildId, channel.id); + }} + onCancel={() => {}} + data-flx="ui.action-menu.items.channel-menu-data.handle-start-p2p-call.voice-p2p-consent-modal" + /> + )), + ); + }, handleOpenMuteSheet: () => { onOpenMuteSheet?.(); }, @@ -654,19 +687,26 @@ export function useChannelMenuData( return menuGroups; } if (guild && (state.isTextChannel || state.isVoiceChannel || state.isLinkChannel || channel.isThreadOnly())) { + const voiceItems: Array = []; if (state.isVoiceChannel && !Accessibility.voiceChannelJoinRequiresDoubleClick) { - menuGroups.push({ - items: [ - { - icon: ( - - ), - label: i18n._(OPEN_CHAT_DESCRIPTOR), - onClick: handlers.handleOpenChat, - }, - ], + voiceItems.push({ + icon: , + label: i18n._(OPEN_CHAT_DESCRIPTOR), + onClick: handlers.handleOpenChat, }); } + if (state.canStartP2pCall) { + voiceItems.push({ + icon: ( + + ), + label: i18n._(VOICE_P2P_START_CALL_DESCRIPTOR), + onClick: handlers.handleStartP2pCall, + }); + } + if (voiceItems.length > 0) { + menuGroups.push({items: voiceItems}); + } const metaItems: Array = []; if (showMarkAsReadItem) { metaItems.push({ diff --git a/fluxer_app/src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx b/fluxer_app/src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx index 62a7a9c3b..5690c884e 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx +++ b/fluxer_app/src/features/user/components/modals/tabs/accessibility_tab/KeyboardTab.tsx @@ -3,6 +3,11 @@ import * as AccessibilityCommands from '@app/features/accessibility/commands/AccessibilityCommands'; import Accessibility from '@app/features/accessibility/state/Accessibility'; import {Switch} from '@app/features/ui/components/form/FormSwitch'; +import VoicePrompts from '@app/features/voice/state/VoicePrompts'; +import { + VOICE_P2P_ALWAYS_AGREE_SETTING_DESCRIPTION_DESCRIPTOR, + VOICE_P2P_ALWAYS_AGREE_SETTING_DESCRIPTOR, +} from '@app/features/voice/utils/VoiceMessageDescriptors'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {observer} from 'mobx-react-lite'; @@ -30,6 +35,7 @@ export const KeyboardTabContent: React.FC = observer(() => { const escapeExitsKeyboardMode = Accessibility.escapeExitsKeyboardMode; const showContextMenuShortcuts = Accessibility.showContextMenuShortcuts; const confirmBeforeStartingCalls = Accessibility.confirmBeforeStartingCalls; + const skipP2pJoinConfirm = VoicePrompts.skipP2pJoinConfirm; return ( <> { onChange={(value) => AccessibilityCommands.update({confirmBeforeStartingCalls: value})} data-flx="user.accessibility-tab.keyboard-tab.keyboard-tab-content.switch.update--4" /> + VoicePrompts.setSkipP2pJoinConfirm(value)} + data-flx="user.accessibility-tab.keyboard-tab.keyboard-tab-content.switch.skip-p2p-join-confirm" + /> ); }); diff --git a/fluxer_app/src/features/voice/components/StreamSettingsMenuContent.tsx b/fluxer_app/src/features/voice/components/StreamSettingsMenuContent.tsx index e8cf518b1..4dbccbd38 100644 --- a/fluxer_app/src/features/voice/components/StreamSettingsMenuContent.tsx +++ b/fluxer_app/src/features/voice/components/StreamSettingsMenuContent.tsx @@ -192,6 +192,7 @@ const PremiumBadge = () => ( ); export function useHasHigherVideoQuality(): boolean { + useMediaEngineVersion(); return resolveHigherVideoQuality(); } diff --git a/fluxer_app/src/features/voice/components/VoiceE2EEIndicator.module.css b/fluxer_app/src/features/voice/components/VoiceE2EEIndicator.module.css index 638a591ce..d02601598 100644 --- a/fluxer_app/src/features/voice/components/VoiceE2EEIndicator.module.css +++ b/fluxer_app/src/features/voice/components/VoiceE2EEIndicator.module.css @@ -18,3 +18,15 @@ composes: indicatorBase; color: var(--status-danger); } + +.indicatorP2p { + composes: indicatorEncrypted; + width: fit-content; + margin-inline: auto; +} + +.indicatorP2pBroken { + composes: indicatorBroken; + width: fit-content; + margin-inline: auto; +} diff --git a/fluxer_app/src/features/voice/components/VoiceE2EEIndicator.tsx b/fluxer_app/src/features/voice/components/VoiceE2EEIndicator.tsx index da4d8977f..10f7926e1 100644 --- a/fluxer_app/src/features/voice/components/VoiceE2EEIndicator.tsx +++ b/fluxer_app/src/features/voice/components/VoiceE2EEIndicator.tsx @@ -1,13 +1,18 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import styles from '@app/features/voice/components/VoiceE2EEIndicator.module.css'; import MediaEngine, {useMediaEngineVersion} from '@app/features/voice/engine/MediaEngineFacade'; +import VoiceMeshPeers from '@app/features/voice/engine/mesh/VoiceMeshPeers'; import {computeChannelE2EEStatus} from '@app/features/voice/state/ChannelE2EEStatus'; +import {isChannelP2p} from '@app/features/voice/state/ChannelP2pStatus'; import { VOICE_CALL_E2EE_BROKEN_DESCRIPTOR, VOICE_CALL_E2EE_ENCRYPTED_DESCRIPTOR, VOICE_CHANNEL_E2EE_BROKEN_DESCRIPTOR, VOICE_CHANNEL_E2EE_ENCRYPTED_DESCRIPTOR, + VOICE_P2P_INDICATOR_DESCRIPTOR, + VOICE_P2P_INDICATOR_TOOLTIP_DESCRIPTOR, } from '@app/features/voice/utils/VoiceMessageDescriptors'; import {useLingui} from '@lingui/react/macro'; import {observer} from 'mobx-react-lite'; @@ -26,6 +31,24 @@ export const VoiceE2EEIndicator = observer(function VoiceE2EEIndicator({ const {i18n} = useLingui(); useMediaEngineVersion(); void MediaEngine.getAllVoiceStates(); + if (isChannelP2p(guildId, channelId)) { + const isBroken = MediaEngine.channelId === channelId && VoiceMeshPeers.hasFailedPeer; + return ( + +
+ {i18n._(VOICE_P2P_INDICATOR_DESCRIPTOR)} +
+
+ ); + } const gatewayStatus = computeChannelE2EEStatus(guildId, channelId, {emptyChannelStatus: 'encrypted'}); const status = gatewayStatus; if (status === 'none') return null; diff --git a/fluxer_app/src/features/voice/components/VoiceSettingsMenus.tsx b/fluxer_app/src/features/voice/components/VoiceSettingsMenus.tsx index e451a17f8..42486e1cd 100644 --- a/fluxer_app/src/features/voice/components/VoiceSettingsMenus.tsx +++ b/fluxer_app/src/features/voice/components/VoiceSettingsMenus.tsx @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals'; +import Channels from '@app/features/channel/state/Channels'; import * as VoiceStateCommands from '@app/features/devtools/commands/VoiceStateCommands'; import {CAMERA_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import Keybind from '@app/features/input/state/InputKeybind'; @@ -30,6 +31,7 @@ import { getPrimaryLocalMicrophonePublication, } from '@app/features/voice/engine/VoiceTrackPublicationUtils'; import CallState from '@app/features/voice/state/CallState'; +import {isActiveVoiceChannelP2p} from '@app/features/voice/state/ChannelP2pStatus'; import VoiceCallLayout from '@app/features/voice/state/VoiceCallLayout'; import VoicePrompts from '@app/features/voice/state/VoicePrompts'; import VoiceSettings from '@app/features/voice/state/VoiceSettings'; @@ -46,6 +48,7 @@ import { VOICE_INPUT_VOLUME_DESCRIPTOR, VOICE_OUTPUT_DEVICE_DESCRIPTOR, VOICE_OUTPUT_VOLUME_DESCRIPTOR, + VOICE_P2P_SWITCH_TO_STANDARD_DESCRIPTOR, } from '@app/features/voice/utils/VoiceMessageDescriptors'; import {getActiveVoiceProcessingMode, type VoiceProcessingMode} from '@app/features/voice/utils/VoiceProcessingProfile'; import {VOICE_VOLUME_MAX_PERCENT} from '@app/features/voice/utils/VoiceVolumeUtils'; @@ -54,6 +57,7 @@ import type {RtcRegionResponse} from '@fluxer/schema/src/domains/channel/Channel import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; import { + ArrowsLeftRightIcon, CameraIcon, ChartBarIcon, EyeIcon, @@ -656,6 +660,9 @@ export const VoiceMoreOptionsMenu: React.FC = observe (Users.currentUser?.isStaff() ?? false) && VoiceDebugEventSinkCommands.canOpenVoiceDebugEventSinkPopout(); const isDmVoiceCall = connectedChannelId != null && (MediaEngine.guildId ?? null) === null; + const connectedChannel = connectedChannelId != null ? Channels.getChannel(connectedChannelId) : undefined; + void MediaEngine.getAllVoiceStates(); + const canSwitchToStandardCall = isActiveVoiceChannelP2p() && (isDmVoiceCall || !connectedChannel?.rtcP2p); const currentRegion = isDmVoiceCall && connectedChannelId ? (CallState.getCall(connectedChannelId)?.region ?? AUTOMATIC_VOICE_REGION_ID) @@ -846,6 +853,24 @@ export const VoiceMoreOptionsMenu: React.FC = observe > {i18n._(PRIORITIZE_SPEAKERS_DESCRIPTOR)} + {canSwitchToStandardCall && ( + + } + onClick={() => { + MediaEngine.switchToStandardCall(); + onClose(); + }} + data-flx="voice.voice-settings-menus.voice-more-options-menu.menu-item.switch-to-standard-call" + > + {i18n._(VOICE_P2P_SWITCH_TO_STANDARD_DESCRIPTOR)} + + )} {canOpenDebugEventSink && ( void; + onStandard?: () => void; + onCancel: () => void; +} + +export const VoiceP2pConsentModal = observer( + ({intent, allowStandard, onP2p, onStandard, onCancel}: VoiceP2pConsentModalProps) => { + const {i18n} = useLingui(); + const [alwaysAgree, setAlwaysAgree] = useState(false); + const initialFocusRef = useRef(null); + const isJoin = intent === 'join'; + const showStandard = !isJoin && allowStandard && onStandard != null; + const handleP2p = () => { + if (isJoin && alwaysAgree) VoicePrompts.setSkipP2pJoinConfirm(true); + ModalCommands.pop(); + onP2p(); + }; + const handleStandard = () => { + ModalCommands.pop(); + onStandard?.(); + }; + const handleCancel = () => { + ModalCommands.pop(); + onCancel(); + }; + return ( + + + + + + {i18n._(VOICE_P2P_DIRECT_MEDIA_DESCRIPTOR)} + +
    +
  • + {i18n._(VOICE_P2P_LOWER_LATENCY_DESCRIPTOR)} +
  • +
  • + {i18n._(VOICE_P2P_STREAM_QUALITY_DESCRIPTOR)} +
  • +
  • + {i18n._(VOICE_P2P_IP_ADDRESS_DESCRIPTOR)} +
  • +
  • + {i18n._(VOICE_P2P_MAX_PARTICIPANTS_DESCRIPTOR, {maxParticipants: VoiceP2pRollout.maxParticipants})} +
  • +
  • + {i18n._(VOICE_P2P_CONNECT_DIRECTLY_DESCRIPTOR)} +
  • +
+ {isJoin && ( + setAlwaysAgree(checked)} + size="small" + data-flx="voice.voice-p2p-consent-modal.checkbox.always-agree" + > + + {i18n._(VOICE_P2P_ALWAYS_AGREE_DESCRIPTOR)} + + + )} +
+
+ + + {showStandard && ( + + )} + + +
+ ); + }, +); diff --git a/fluxer_app/src/features/voice/components/alerts/VoiceP2pPeerFailedModal.tsx b/fluxer_app/src/features/voice/components/alerts/VoiceP2pPeerFailedModal.tsx new file mode 100644 index 000000000..640a4a420 --- /dev/null +++ b/fluxer_app/src/features/voice/components/alerts/VoiceP2pPeerFailedModal.tsx @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import * as Modal from '@app/features/app/components/dialogs/Modal'; +import {Button} from '@app/features/ui/button/Button'; +import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; +import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; +import { + VOICE_P2P_LEAVE_DESCRIPTOR, + VOICE_P2P_PEER_FAILED_DESCRIPTION_DESCRIPTOR, + VOICE_P2P_PEER_FAILED_SWITCH_HINT_DESCRIPTOR, + VOICE_P2P_PEER_FAILED_TITLE_DESCRIPTOR, + VOICE_P2P_STAY_DESCRIPTOR, + VOICE_P2P_SWITCH_TO_STANDARD_DESCRIPTOR, +} from '@app/features/voice/utils/VoiceMessageDescriptors'; +import {useLingui} from '@lingui/react/macro'; +import {formatListWithConfig} from '@pkgs/list_utils/src/ListFormatting'; +import {observer} from 'mobx-react-lite'; + +export interface VoiceP2pPeerFailedModalProps { + names: ReadonlyArray; + allowSwitch: boolean; + onLeave: () => void; + onSwitch: () => void; +} + +export const VoiceP2pPeerFailedModal = observer( + ({names, allowSwitch, onLeave, onSwitch}: VoiceP2pPeerFailedModalProps) => { + const {i18n} = useLingui(); + const formattedNames = formatListWithConfig(names, { + locale: getCurrentLocale(), + style: 'long', + type: 'conjunction', + }); + const handleLeave = () => { + ModalCommands.pop(); + onLeave(); + }; + const handleSwitch = () => { + ModalCommands.pop(); + onSwitch(); + }; + return ( + + + + + + {allowSwitch + ? `${i18n._(VOICE_P2P_PEER_FAILED_DESCRIPTION_DESCRIPTOR)} ${i18n._(VOICE_P2P_PEER_FAILED_SWITCH_HINT_DESCRIPTOR)}` + : i18n._(VOICE_P2P_PEER_FAILED_DESCRIPTION_DESCRIPTOR)} + + + + + + + {allowSwitch && ( + + )} + + + ); + }, +); diff --git a/fluxer_app/src/features/voice/engine/MediaEngineFacade.ts b/fluxer_app/src/features/voice/engine/MediaEngineFacade.ts index 432206385..1a3a5056d 100644 --- a/fluxer_app/src/features/voice/engine/MediaEngineFacade.ts +++ b/fluxer_app/src/features/voice/engine/MediaEngineFacade.ts @@ -17,18 +17,25 @@ import GuildMembers from '@app/features/member/state/GuildMembers'; import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands'; import {SoundType, setSoundOutputDeviceIdResolver} from '@app/features/notification/utils/SoundUtils'; import MediaPermission from '@app/features/permissions/system/state/MediaPermission'; +import NativePermission from '@app/features/permissions/system/state/NativePermission'; import {Logger} from '@app/features/platform/utils/AppLogger'; +import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; +import {modal} from '@app/features/ui/commands/ModalCommands'; import * as SoundCommands from '@app/features/ui/commands/SoundCommands'; import * as ToastCommands from '@app/features/ui/commands/ToastCommands'; import Idle from '@app/features/ui/state/Idle'; import Sound from '@app/features/ui/state/Sound'; -import {getElectronAPI} from '@app/features/ui/utils/NativeUtils'; +import {getElectronAPI, isDesktop} from '@app/features/ui/utils/NativeUtils'; import Users from '@app/features/user/state/Users'; +import * as NicknameUtils from '@app/features/user/utils/NicknameUtils'; +import {VoiceP2pPeerFailedModal} from '@app/features/voice/components/alerts/VoiceP2pPeerFailedModal'; import {getStreamKey, parseStreamKey} from '@app/features/voice/components/StreamKeys'; +import {pushActiveStreamSettings} from '@app/features/voice/components/StreamSettingsMenuContent'; import {voiceStatsDB} from '@app/features/voice/diagnostics/VoiceStatsDB'; import { createMediaEngineFacadeSnapshot, type MediaEngineFacadeEvent, + type MediaEngineFacadeP2pRefusal, type MediaEngineFacadePendingSessionRestore, type MediaEngineFacadeSnapshot, selectMediaEngineConnectPreflightDecision, @@ -56,6 +63,8 @@ import { saveCurrentVoiceSessionRestoreSnapshot, type VoiceSessionRestoreSyncHandle, } from '@app/features/voice/engine/media_engine_facade/VoiceSessionRestoreSync'; +import VoiceMeshPeers from '@app/features/voice/engine/mesh/VoiceMeshPeers'; +import {isVoiceMeshRoom} from '@app/features/voice/engine/mesh/VoiceMeshRoom'; import ScreenShareCodecNegotiation from '@app/features/voice/engine/ScreenShareCodecNegotiation'; import ScreenSharePublicationMigration from '@app/features/voice/engine/ScreenSharePublicationMigration'; import {Store, useStoreVersion} from '@app/features/voice/engine/Store'; @@ -63,8 +72,11 @@ import {shouldMoveToAfkOnTick} from '@app/features/voice/engine/VoiceAfkTracking import { checkChannelLimit, checkMultipleConnections, + checkP2pConsent, + requestP2pConsent, sendVoiceStateConnect, sendVoiceStateDisconnect, + showVoiceChannelFullModal, syncVoiceStateToServer, } from '@app/features/voice/engine/VoiceChannelConnector'; import type { @@ -102,6 +114,7 @@ import { } from '@app/features/voice/engine/VoiceStreamWatchState'; import {type VoiceConnectionQuality, VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource'; import {bindVoiceEngineV2AppAudioPreferencesSync} from '@app/features/voice/engine/v2/VoiceEngineV2AppAudioPreferencesSyncBinding'; +import {getCameraCaptureDimensions} from '@app/features/voice/engine/v2/VoiceEngineV2AppCameraResolutionPresets'; import voiceEngineV2AppConnectionHostAdapter, { type VoiceServerUpdateData, } from '@app/features/voice/engine/v2/VoiceEngineV2AppConnectionHostAdapter'; @@ -138,15 +151,23 @@ import {VoiceEngineV2AppStatsHostAdapter} from '@app/features/voice/engine/v2/Vo import VoiceEngineV2AppSubscriptionAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppSubscriptionAdapter'; import voiceEngineV2AppVoiceStateAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppVoiceStateAdapter'; import type {DisplayScreenShareCaptureContext} from '@app/features/voice/engine/voice_screen_share_manager/shared'; +import ActiveScreenShareSource from '@app/features/voice/state/ActiveScreenShareSource'; import CallMediaPrefs from '@app/features/voice/state/CallMediaPrefs'; import {type ChannelE2EEStatus, computeChannelE2EEStatus} from '@app/features/voice/state/ChannelE2EEStatus'; import LocalVoiceState from '@app/features/voice/state/LocalVoiceState'; import VoiceCallLayout from '@app/features/voice/state/VoiceCallLayout'; +import VoiceP2pConsent from '@app/features/voice/state/VoiceP2pConsent'; import VoiceRegionTeleport from '@app/features/voice/state/VoiceRegionTeleport'; import VoiceSessionRestore, {type VoiceSessionRestoreSnapshot} from '@app/features/voice/state/VoiceSessionRestore'; import VoiceSettings from '@app/features/voice/state/VoiceSettings'; import {setNativeAudioCaptureBridgeLifecycleBridge} from '@app/features/voice/utils/NativeAudioCaptureBridge'; +import {resolveDisplayShareEnvironment} from '@app/features/voice/utils/ScreenShareEnvironment'; +import {executeScreenShareOperation} from '@app/features/voice/utils/ScreenShareUtils'; import {areOrderedStringArraysEqual} from '@app/features/voice/utils/StringArrayUtils'; +import { + VOICE_P2P_CONSENT_REQUIRED_DESCRIPTOR, + VOICE_P2P_UNAVAILABLE_DESCRIPTOR, +} from '@app/features/voice/utils/VoiceMessageDescriptors'; import {buildVoiceParticipantIdentity} from '@app/features/voice/utils/VoiceParticipantIdentity'; import { isVoiceServerMuteActive, @@ -182,9 +203,11 @@ import type { ScreenShareCaptureOptions, TrackPublishOptions, } from 'livekit-client'; -import {makeObservable, observableRef} from 'mobx'; +import {compareStructural, makeObservable, observableRef, reaction} from 'mobx'; +import React from 'react'; const logger = new Logger('MediaEngineFacade'); +const VOICE_P2P_PEER_FAILED_MODAL_KEY = 'voice-p2p-peer-failed'; function isCameraPermissionDeniedCommandFailure(error: unknown): boolean { if (!(error instanceof Error)) return false; @@ -411,6 +434,7 @@ class MediaEngineFacade extends Store { this.initializeLocalAudioStateSync(); this.initializeVoiceSessionRestoreSync(); this.initializeE2EEStatusSync(); + this.initializeP2pPeerFailedModalSync(); this.initializeTerminalUnloadVoiceDisconnect(); Sound.setSelfDeafenedResolver(() => getEffectiveAudioState().effectiveDeaf); setSoundOutputDeviceIdResolver(() => VoiceSettings.getOutputDeviceId()); @@ -877,7 +901,7 @@ class MediaEngineFacade extends Store { } previousRoom = room; previousStatus = status; - if (!room) return; + if (!room || isVoiceMeshRoom(room)) return; if (status === 'encrypted') { void room.setE2EEEnabled(true).catch((error) => { logger.warn('Failed to enable E2EE on room', error); @@ -892,6 +916,51 @@ class MediaEngineFacade extends Store { voiceEngineV2AppVoiceStateAdapter.subscribe(syncE2EEStatus); } + private initializeP2pPeerFailedModalSync(): void { + reaction( + () => VoiceMeshPeers.failedConnectionIds, + (failedConnectionIds, previousConnectionIds) => { + if (failedConnectionIds.length === 0) { + ModalCommands.popWithKey(VOICE_P2P_PEER_FAILED_MODAL_KEY); + return; + } + if (failedConnectionIds.some((connectionId) => !previousConnectionIds.includes(connectionId))) { + this.showP2pPeerFailedModal(failedConnectionIds); + } + }, + {equals: compareStructural}, + ); + } + + private showP2pPeerFailedModal(failedConnectionIds: ReadonlyArray): void { + const {guildId, channelId} = voiceEngineV2AppConnectionHostAdapter.connectionState; + if (!channelId) return; + const names = failedConnectionIds.flatMap((connectionId) => { + const userId = voiceEngineV2AppVoiceStateAdapter.getVoiceStateByConnectionId(connectionId)?.user_id; + const user = userId ? Users.getUser(userId) : undefined; + return user ? [NicknameUtils.getNickname(user, guildId, channelId)] : []; + }); + const allowSwitch = guildId === null || Channels.getChannel(channelId)?.rtcP2p !== true; + ModalCommands.pushWithKey( + modal(() => + React.createElement(VoiceP2pPeerFailedModal, { + names, + allowSwitch, + onLeave: () => void this.disconnectFromVoiceChannel('user'), + onSwitch: () => this.switchToStandardCall(), + }), + ), + VOICE_P2P_PEER_FAILED_MODAL_KEY, + ); + } + + switchToStandardCall(): void { + const {guildId, channelId, connectionId} = voiceEngineV2AppConnectionHostAdapter.connectionState; + if (!channelId || !connectionId) return; + logger.info('Switching P2P call to a standard call', {guildId, channelId}); + syncVoiceStateToServer(guildId, channelId, connectionId, undefined, false); + } + private initializeVoiceSessionRestoreSync(): void { this.voiceSessionRestoreSync?.dispose(); this.voiceSessionRestoreSync = createVoiceSessionRestoreSync(); @@ -1143,6 +1212,13 @@ class MediaEngineFacade extends Store { if (channelLimitDecision.type === 'abort') { return; } + const hasP2pConsent = checkP2pConsent( + resolvedGuildId, + channelId, + () => void this.connectToVoiceChannel(guildId, channelId, options), + () => voiceEngineV2AppConnectionHostAdapter.clearInFlightConnect(), + ); + if (!hasP2pConsent) return; const shouldProceed = checkMultipleConnections( resolvedGuildId, channelId, @@ -1322,6 +1398,7 @@ class MediaEngineFacade extends Store { this.statsHostAdapter.reset(); if (reason === 'user' || reason === 'server') { VoiceSessionRestore.clearSnapshot(); + VoiceP2pConsent.clear(); } if (reason !== 'server' && connectionId) { sendVoiceStateDisconnect(guildId, connectionId); @@ -1490,6 +1567,7 @@ class MediaEngineFacade extends Store { if (raw.connection_id && raw.connection_id === this.pendingServerDisconnectConnectionId) { this.cancelPendingServerDisconnect(); } + if (!raw.ice_servers) VoiceP2pConsent.clear(); this.handleVoiceServerUpdateViaJs(raw); } @@ -1537,6 +1615,8 @@ class MediaEngineFacade extends Store { Boolean(this.pendingSessionRestore) && this.pendingSessionRestore?.guildId === resolvedGuildId && this.pendingSessionRestore?.channelId === resolvedChannelId; + const currentRoom = voiceEngineV2AppConnectionHostAdapter.room; + const isConversion = !raw.ice_servers && currentRoom !== null && isVoiceMeshRoom(currentRoom); voiceEngineV2AppConnectionHostAdapter.handleVoiceServerUpdate( raw, (room, attemptId, guildId, channelId) => { @@ -1559,6 +1639,7 @@ class MediaEngineFacade extends Store { if (shouldPreserveLocalMedia) { await this.restoreLocalMediaState(room); } + if (isConversion) await this.reapplyVideoQualityLimits(room); if (shouldApplyPendingSessionRestore) { await this.restorePendingSessionMedia(); } @@ -1629,6 +1710,7 @@ class MediaEngineFacade extends Store { } voiceEngineV2AppMediaExecutionAdapter.resetMicrophoneFailureLatch(); this.reconcileLocalAudioStateInBackground('region hot-swap complete'); + if (isConversion) void this.reapplyVideoQualityLimits(newRoom); }, (_guildId, _channelId, _connectionId, _attemptId, error) => { logger.warn('LiveKit connect failed, clearing gateway voice state', {error}); @@ -2573,6 +2655,7 @@ class MediaEngineFacade extends Store { } handleGatewayError(error: GatewayErrorData): void { + const {guildId, channelId} = voiceEngineV2AppConnectionHostAdapter; const decision = selectMediaEngineGatewayErrorDecision(this.facadeSnapshot, { code: error.code, connecting: voiceEngineV2AppConnectionHostAdapter.connecting, @@ -2611,6 +2694,38 @@ class MediaEngineFacade extends Store { : CLAIM_YOUR_ACCOUNT_TO_JOIN_THIS_VOICE_CHANNEL_DESCRIPTOR; this.showVoiceErrorModal(descriptor, `voice.media-engine-facade.gateway-${decision.toast}-error-modal`); } + if (decision.p2pRefusal) { + this.handleP2pRefusal(decision.p2pRefusal, decision.abortConnection && channelId ? {guildId, channelId} : null); + } + } + + private handleP2pRefusal( + refusal: MediaEngineFacadeP2pRefusal, + target: {guildId: string | null; channelId: string} | null, + ): void { + if (refusal === 'consent-required' && target && !VoiceP2pConsent.isAgreed(target.channelId)) { + const retry = () => void this.connectToVoiceChannel(target.guildId, target.channelId); + if (requestP2pConsent(target.guildId, target.channelId, retry, () => {})) retry(); + return; + } + VoiceP2pConsent.clear(); + switch (refusal) { + case 'consent-required': + this.showVoiceErrorModal( + VOICE_P2P_CONSENT_REQUIRED_DESCRIPTOR, + 'voice.media-engine-facade.gateway-p2p-consent-required-error-modal', + ); + return; + case 'unavailable': + this.showVoiceErrorModal( + VOICE_P2P_UNAVAILABLE_DESCRIPTOR, + 'voice.media-engine-facade.gateway-p2p-unavailable-error-modal', + ); + return; + case 'channel-full': + showVoiceChannelFullModal(); + return; + } } cleanup(): void { @@ -2645,6 +2760,25 @@ class MediaEngineFacade extends Store { voiceEngineV2AppMediaStateAdapter.resetLocalMediaState('cleanup'); } + private async reapplyVideoQualityLimits(room: Room): Promise { + try { + if (room.localParticipant.isCameraEnabled) { + await voiceEngineV2AppMediaExecutionAdapter.updateCameraEncoding( + getCameraCaptureDimensions(VoiceSettings.getCameraResolution()), + ); + } + if (!room.localParticipant.isScreenShareEnabled) return; + await executeScreenShareOperation(() => + pushActiveStreamSettings( + ActiveScreenShareSource.getShareContext() ?? 'display', + resolveDisplayShareEnvironment(isDesktop(), NativePermission.isLinuxWaylandDesktop), + ), + ); + } catch (error) { + logger.warn('Failed to reapply video quality limits after conversion', {error}); + } + } + private async restoreLocalMediaState(roomOverride?: Room | null): Promise { const room = roomOverride ?? voiceEngineV2AppConnectionHostAdapter.room; const participant = room?.localParticipant ?? null; diff --git a/fluxer_app/src/features/voice/engine/MediaEngineFacadeStateMachine.ts b/fluxer_app/src/features/voice/engine/MediaEngineFacadeStateMachine.ts index 3668b39be..f6e295f71 100644 --- a/fluxer_app/src/features/voice/engine/MediaEngineFacadeStateMachine.ts +++ b/fluxer_app/src/features/voice/engine/MediaEngineFacadeStateMachine.ts @@ -92,8 +92,11 @@ export type MediaEngineFacadeGatewayErrorDecision = showUnavailableToast: boolean; disconnectReason: 'server' | null; toast: 'timed-out' | 'connection-limit' | 'unclaimed-account' | null; + p2pRefusal: MediaEngineFacadeP2pRefusal | null; }; +export type MediaEngineFacadeP2pRefusal = 'consent-required' | 'unavailable' | 'channel-full'; + export interface MediaEngineFacadeDeferredDisconnectInput { connectionId: string; currentConnectionId: string | null; @@ -411,6 +414,8 @@ const voiceGatewayErrorCodes = new Set([ GatewayErrorCodes.VOICE_MISSING_CONNECTION_ID, GatewayErrorCodes.VOICE_TOKEN_FAILED, GatewayErrorCodes.VOICE_UNCLAIMED_ACCOUNT, + GatewayErrorCodes.VOICE_P2P_CONSENT_REQUIRED, + GatewayErrorCodes.VOICE_P2P_UNAVAILABLE, ]); const unavailableGatewayErrorCodes = new Set([ @@ -430,6 +435,14 @@ const preConnectAbortGatewayErrorCodes = new Set([ GatewayErrorCodes.VOICE_CONNECTION_LIMIT_REACHED, GatewayErrorCodes.VOICE_MEMBER_TIMED_OUT, GatewayErrorCodes.VOICE_UNCLAIMED_ACCOUNT, + GatewayErrorCodes.VOICE_P2P_CONSENT_REQUIRED, + GatewayErrorCodes.VOICE_P2P_UNAVAILABLE, +]); + +const p2pRefusalsByGatewayErrorCode = new Map([ + [GatewayErrorCodes.VOICE_P2P_CONSENT_REQUIRED, 'consent-required'], + [GatewayErrorCodes.VOICE_P2P_UNAVAILABLE, 'unavailable'], + [GatewayErrorCodes.VOICE_CHANNEL_FULL, 'channel-full'], ]); export function selectMediaEngineGatewayErrorDecision( @@ -456,6 +469,7 @@ export function selectMediaEngineGatewayErrorDecision( showUnavailableToast: unavailableChannelId !== null, disconnectReason: null, toast: null, + p2pRefusal: null, }; } if (unavailableChannelId && input.channelId === unavailableChannelId) { @@ -481,6 +495,7 @@ export function selectMediaEngineGatewayErrorDecision( showUnavailableToast: unavailableChannelId !== null, disconnectReason, toast, + p2pRefusal: p2pRefusalsByGatewayErrorCode.get(input.code) ?? null, }; } diff --git a/fluxer_app/src/features/voice/engine/VoiceChannelConnector.tsx b/fluxer_app/src/features/voice/engine/VoiceChannelConnector.tsx index a05e29e69..cd8bfe298 100644 --- a/fluxer_app/src/features/voice/engine/VoiceChannelConnector.tsx +++ b/fluxer_app/src/features/voice/engine/VoiceChannelConnector.tsx @@ -8,6 +8,7 @@ import {modal} from '@app/features/ui/commands/ModalCommands'; import Users from '@app/features/user/state/Users'; import {VoiceChannelFullModal} from '@app/features/voice/components/alerts/VoiceChannelFullModal'; import {VoiceConnectionConfirmModal} from '@app/features/voice/components/alerts/VoiceConnectionConfirmModal'; +import {VoiceP2pConsentModal} from '@app/features/voice/components/alerts/VoiceP2pConsentModal'; import { getAllVoiceStatesInChannelFromMediaEngine, getVoiceConnectionContextFromMediaEngine, @@ -16,35 +17,97 @@ import { } from '@app/features/voice/engine/VoiceMediaEngineBridge'; import type {VoiceStateSyncPartial} from '@app/features/voice/engine/VoiceStateSyncTypes'; import {selectVoiceEngineV2AppIntentSelfMuteForVoiceStatePayload} from '@app/features/voice/engine/v2/VoiceEngineV2AppSelectors'; +import {isChannelP2p} from '@app/features/voice/state/ChannelP2pStatus'; import LocalVoiceState from '@app/features/voice/state/LocalVoiceState'; +import VoiceP2pConsent from '@app/features/voice/state/VoiceP2pConsent'; +import VoiceP2pRollout from '@app/features/voice/state/VoiceP2pRollout'; +import VoicePrompts from '@app/features/voice/state/VoicePrompts'; import {applyVoiceSpeakPermissionToSelfMute} from '@app/features/voice/utils/VoicePermissionUtils'; import {ME} from '@fluxer/constants/src/AppConstants'; import {VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT} from '@fluxer/constants/src/LimitConstants'; const logger = new Logger('VoiceChannelConnector'); -export function checkChannelLimit(guildId: string | null, channelId: string): boolean { - if (!guildId) return true; - const channel = Channels.getChannel(channelId); - if (!channel?.userLimit || channel.userLimit <= 0) return true; - const voiceStates = getAllVoiceStatesInChannelFromMediaEngine(guildId, channelId); +export function showVoiceChannelFullModal(): void { + ModalCommands.push( + modal(() => ( + + )), + ); +} + +function countOtherVoiceConnections(guildId: string | null, channelId: string): number { + const voiceStates = getAllVoiceStatesInChannelFromMediaEngine(guildId ?? ME, channelId); const currentConnectionId = getVoiceConnectionContextFromMediaEngine()?.connectionId; - let adjusted = 0; + const currentUserId = Users.getCurrentUser()?.id; + let count = 0; for (const connectionId in voiceStates) { const voiceState = voiceStates[connectionId]; if (!voiceState) continue; if (voiceState.connection_id === currentConnectionId) continue; - adjusted += 1; + if (guildId === null && voiceState.user_id === currentUserId) continue; + count += 1; } - if (adjusted >= channel.userLimit) { - ModalCommands.push( - modal(() => ( - - )), - ); - return false; + return count; +} + +export function checkChannelLimit(guildId: string | null, channelId: string): boolean { + const limits: Array = []; + const userLimit = guildId ? Channels.getChannel(channelId)?.userLimit : undefined; + if (userLimit && userLimit > 0) limits.push(userLimit); + if (isChannelP2p(guildId, channelId)) limits.push(VoiceP2pRollout.maxParticipants); + if (limits.length === 0) return true; + if (countOtherVoiceConnections(guildId, channelId) < Math.min(...limits)) return true; + showVoiceChannelFullModal(); + return false; +} + +function isEmptyPinnedP2pChannel(guildId: string | null, channelId: string): boolean { + return ( + VoiceP2pRollout.enabled && + guildId !== null && + Channels.getChannel(channelId)?.rtcP2p === true && + countOtherVoiceConnections(guildId, channelId) === 0 + ); +} + +export function requestP2pConsent( + guildId: string | null, + channelId: string, + onAgree: () => void, + onCancel: () => void, +): boolean { + if (VoiceP2pConsent.isAgreed(channelId)) return true; + if (VoicePrompts.getSkipP2pJoinConfirm()) { + VoiceP2pConsent.agree(channelId); + return true; } - return true; + const intent = isEmptyPinnedP2pChannel(guildId, channelId) ? 'start' : 'join'; + ModalCommands.push( + modal(() => ( + { + VoiceP2pConsent.agree(channelId); + onAgree(); + }} + onCancel={onCancel} + data-flx="voice.engine.voice-channel-connector.request-p2p-consent.voice-p2p-consent-modal" + /> + )), + ); + return false; +} + +export function checkP2pConsent( + guildId: string | null, + channelId: string, + onAgree: () => void, + onCancel: () => void, +): boolean { + if (!isChannelP2p(guildId, channelId) && !isEmptyPinnedP2pChannel(guildId, channelId)) return true; + return requestP2pConsent(guildId, channelId, onAgree, onCancel); } function resolveVoiceConnectionLimit(guildId: string | null, channelId: string): number { @@ -121,6 +184,7 @@ export function sendVoiceStateConnect( : LocalVoiceState.getSelfMute(); const selfMute = applyVoiceSpeakPermissionToSelfMute(guildId, channelId, effectiveSelfMute); const connectionId = getVoiceConnectionContextFromMediaEngine()?.connectionId ?? null; + VoiceP2pConsent.commit(channelId); socket.updateVoiceStateExplicit({ guild_id: guildId, channel_id: channelId, @@ -130,6 +194,7 @@ export function sendVoiceStateConnect( self_stream: false, viewer_stream_keys: viewerStreamKeys, connection_id: connectionId, + ...(VoiceP2pConsent.isAgreed(channelId) ? {p2p: true} : {}), }); } @@ -156,6 +221,7 @@ export function syncVoiceStateToServer( channelId: string, connectionId: string, partial?: VoiceStateSyncPartial, + p2p?: false, ): void { const socket = GatewayConnection.socket; if (!socket) return; @@ -173,5 +239,6 @@ export function syncVoiceStateToServer( self_stream: partial?.self_stream ?? LocalVoiceState.getSelfStream(), viewer_stream_keys: partial?.viewer_stream_keys ?? LocalVoiceState.getViewerStreamKeys(), connection_id: connectionId, + ...(p2p === undefined ? {} : {p2p}), }); } diff --git a/fluxer_app/src/features/voice/engine/VoiceRoomEventBinder.ts b/fluxer_app/src/features/voice/engine/VoiceRoomEventBinder.ts index b20a8ff92..cf38b49cf 100644 --- a/fluxer_app/src/features/voice/engine/VoiceRoomEventBinder.ts +++ b/fluxer_app/src/features/voice/engine/VoiceRoomEventBinder.ts @@ -3,6 +3,7 @@ import {SoundType} from '@app/features/notification/utils/SoundUtils'; import {Logger} from '@app/features/platform/utils/AppLogger'; import * as SoundCommands from '@app/features/ui/commands/SoundCommands'; +import {isVoiceMeshRoom} from '@app/features/voice/engine/mesh/VoiceMeshRoom'; import ScreenShareCodecNegotiation from '@app/features/voice/engine/ScreenShareCodecNegotiation'; import ScreenSharePublicationMigration from '@app/features/voice/engine/ScreenSharePublicationMigration'; import {getEffectiveAudioState} from '@app/features/voice/engine/VoiceEffectiveAudioState'; @@ -234,7 +235,7 @@ export function bindRoomEvents( screenShareMigrationDisposer = null; }; const bindScreenShareNegotiation = (): void => { - if (screenShareNegotiationBound) return; + if (screenShareNegotiationBound || isVoiceMeshRoom(room)) return; screenShareNegotiationBound = true; bindCodecNegotiation(); bindScreenShareMigration(); diff --git a/fluxer_app/src/features/voice/engine/mesh/VoiceMeshConnectionReports.ts b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshConnectionReports.ts new file mode 100644 index 000000000..a5f3c3203 --- /dev/null +++ b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshConnectionReports.ts @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {Endpoints} from '@app/features/app/constants/Endpoints'; +import {http} from '@app/features/platform/transport/RestTransport'; +import {Logger} from '@app/features/platform/utils/AppLogger'; + +const logger = new Logger('VoiceMeshConnectionReports'); + +const FLUSH_DELAY_MS = 10_000; +const MAX_REPORTS_PER_REQUEST = 8; +const CANDIDATE_TYPES = ['host', 'srflx', 'prflx', 'relay'] as const; +const PROTOCOLS = ['udp', 'tcp'] as const; +const IPV4_ADDRESS = /^\d{1,3}(?:\.\d{1,3}){3}$/u; + +type VoiceMeshCandidateType = (typeof CANDIDATE_TYPES)[number]; + +export interface VoiceMeshConnectionPath { + local_candidate_type: VoiceMeshCandidateType | null; + remote_candidate_type: VoiceMeshCandidateType | null; + ip_family: 'ipv4' | 'ipv6' | null; + protocol: (typeof PROTOCOLS)[number] | null; +} + +export interface VoiceMeshConnectionReport extends VoiceMeshConnectionPath { + channel_id: string; + guild_id: string | null; + participant_count: number; + outcome: 'connected' | 'failed'; + setup_ms: number | null; + ice_restarted: boolean; +} + +export const UNKNOWN_VOICE_MESH_CONNECTION_PATH: VoiceMeshConnectionPath = { + local_candidate_type: null, + remote_candidate_type: null, + ip_family: null, + protocol: null, +}; + +let pending: Array = []; +let flushTimer: ReturnType | null = null; + +function readEnum(values: ReadonlyArray, value: unknown): T | null { + return values.find((candidate) => candidate === value) ?? null; +} + +function readIpFamily(address: unknown): VoiceMeshConnectionPath['ip_family'] { + if (typeof address !== 'string') return null; + if (IPV4_ADDRESS.test(address)) return 'ipv4'; + return address.includes(':') ? 'ipv6' : null; +} + +export function readVoiceMeshConnectionPath(report: RTCStatsReport): VoiceMeshConnectionPath { + const stats = new Map>(); + report.forEach((stat: Record) => stats.set(String(stat.id), stat)); + const entries = Array.from(stats.values()); + const transport = entries.find( + (stat) => stat.type === 'transport' && typeof stat.selectedCandidatePairId === 'string', + ); + const pair = transport + ? stats.get(String(transport.selectedCandidatePairId)) + : entries.find((stat) => stat.type === 'candidate-pair' && stat.nominated === true && stat.state === 'succeeded'); + if (!pair) return UNKNOWN_VOICE_MESH_CONNECTION_PATH; + const local = stats.get(String(pair.localCandidateId)); + const remote = stats.get(String(pair.remoteCandidateId)); + return { + local_candidate_type: readEnum(CANDIDATE_TYPES, local?.candidateType), + remote_candidate_type: readEnum(CANDIDATE_TYPES, remote?.candidateType), + ip_family: readIpFamily(local?.address) ?? readIpFamily(remote?.address), + protocol: readEnum(PROTOCOLS, local?.protocol), + }; +} + +export function queueVoiceMeshConnectionReport(report: VoiceMeshConnectionReport): void { + pending.push(report); + if (pending.length >= MAX_REPORTS_PER_REQUEST) { + flushVoiceMeshConnectionReports(); + return; + } + flushTimer ??= setTimeout(flushVoiceMeshConnectionReports, FLUSH_DELAY_MS); +} + +export function flushVoiceMeshConnectionReports(): void { + if (flushTimer) clearTimeout(flushTimer); + flushTimer = null; + const reports = pending; + pending = []; + if (reports.length === 0) return; + http.post(Endpoints.VOICE_P2P_CONNECTION_REPORTS, {body: {reports}}).catch((error: unknown) => { + logger.debug('Dropped P2P connection reports', {count: reports.length, error}); + }); +} diff --git a/fluxer_app/src/features/voice/engine/mesh/VoiceMeshEngine.ts b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshEngine.ts new file mode 100644 index 000000000..66f3054d5 --- /dev/null +++ b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshEngine.ts @@ -0,0 +1,680 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import GatewayConnection from '@app/features/gateway/transport/GatewayConnection'; +import {VoiceSignalSendResult} from '@app/features/gateway/transport/GatewaySocket'; +import {Logger} from '@app/features/platform/utils/AppLogger'; +import { + flushVoiceMeshConnectionReports, + queueVoiceMeshConnectionReport, + readVoiceMeshConnectionPath, + UNKNOWN_VOICE_MESH_CONNECTION_PATH, +} from '@app/features/voice/engine/mesh/VoiceMeshConnectionReports'; +import {VoiceMeshPeer} from '@app/features/voice/engine/mesh/VoiceMeshPeer'; +import VoiceMeshPeers from '@app/features/voice/engine/mesh/VoiceMeshPeers'; +import { + VOICE_MESH_TRACK_KINDS, + VOICE_MESH_TRACK_SOURCES, + VoiceMeshSender, +} from '@app/features/voice/engine/mesh/VoiceMeshSender'; +import { + sendVoiceMeshSignal, + subscribeVoiceMeshSignals, + VOICE_MESH_SOURCES, + VOICE_MESH_TOKEN, + type VoiceMeshIncomingSignal, + type VoiceMeshSignalData, + type VoiceMeshSource, + type VoiceMeshTrackInfo, + voiceMeshTrackSid, +} from '@app/features/voice/engine/mesh/VoiceMeshSignal'; +import {VoiceMeshSignalClient} from '@app/features/voice/engine/mesh/VoiceMeshSignalClient'; +import type {NormalizedVoiceState} from '@app/features/voice/engine/VoiceGatewayStateMachine'; +import {getSharedVoiceAudioContext} from '@app/features/voice/engine/VoiceSharedAudioContext'; +import { + getRemoteSpeakingThresholdRms, + SPEAKING_REMOTE_RELEASE_MS, +} from '@app/features/voice/engine/VoiceSpeakingThreshold'; +import {computeTimeDomainRms} from '@app/features/voice/engine/v2/VoiceEngineV2AppRemoteSpeakingAdapter'; +import voiceEngineV2AppVoiceStateAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppVoiceStateAdapter'; +import VoiceSettings from '@app/features/voice/state/VoiceSettings'; +import {buildVoiceParticipantIdentity} from '@app/features/voice/utils/VoiceParticipantIdentity'; +import {ME} from '@fluxer/constants/src/AppConstants'; +import { + type AddTrackRequest, + Codec, + DisconnectReason, + EngineEvent, + type InternalRoomOptions, + JoinResponse, + type LocalTrack, + LocalVideoTrack, + ParticipantInfo, + ParticipantInfo_State, + ParticipantPermission, + type PCTransportManager, + PCTransportState, + type Room, + RTCEngine, + type SignalOptions, + SpeakerInfo, + Track, + TrackInfo, + TrackInvalidError, + type TrackPublishOptions, +} from 'livekit-client'; +import {reaction} from 'mobx'; + +const logger = new Logger('VoiceMeshEngine'); + +const INBOX_LIMIT = 64; +const CONVERSION_GRACE_MS = 10_000; +const SPEAKING_TICK_MS = 50; +const SCREEN_SHARE_BUDGET_BPS = 12_000_000; +const CAMERA_BUDGET_BPS = 4_000_000; +const MESH_VIDEO_CODEC_MIME_TYPES = ['video/H264', 'video/VP8']; + +export interface VoiceMeshTarget { + iceServers: Array>; + guildId: string | null; + channelId: string; + connectionId: string; + userId: string; +} + +interface SpeakingAnalyser { + track: MediaStreamTrack; + source: MediaStreamAudioSourceNode; + analyser: AnalyserNode; + samples: Float32Array; + timer: ReturnType; + lastLoudAt: number | null; + active: boolean; +} + +let activeEngine: VoiceMeshEngine | null = null; + +function mergeStatsReports(reports: ReadonlyArray): RTCStatsReport { + const merged = new Map>(); + for (const [prefix, report] of reports) { + report.forEach((stat: Record) => { + const entry: Record = {...stat}; + for (const [key, value] of Object.entries(entry)) { + if ((key === 'id' || key.endsWith('Id')) && typeof value === 'string') entry[key] = `${prefix}:${value}`; + } + merged.set(String(entry.id), entry); + }); + } + return merged as unknown as RTCStatsReport; +} + +function applySendParameters( + target: RTCRtpSendParameters, + requested: RTCRtpSendParameters, + bitrateCap: number | undefined, +): boolean { + const encoding = target.encodings[0]; + if (!encoding) return false; + const top = requested.encodings.findLast((candidate) => candidate.active !== false); + const maxBitrate = bitrateCap === undefined ? top?.maxBitrate : Math.min(top?.maxBitrate ?? bitrateCap, bitrateCap); + let changed = false; + const assign = (key: K, value: RTCRtpEncodingParameters[K] | undefined) => { + if (value === undefined || encoding[key] === value) return; + encoding[key] = value; + changed = true; + }; + assign('active', top !== undefined); + assign('maxBitrate', maxBitrate); + assign('maxFramerate', top?.maxFramerate); + assign('scaleResolutionDownBy', top?.scaleResolutionDownBy); + assign('priority', top?.priority); + assign('networkPriority', top?.networkPriority); + if ( + requested.degradationPreference !== undefined && + target.degradationPreference !== requested.degradationPreference + ) { + target.degradationPreference = requested.degradationPreference; + changed = true; + } + return changed; +} + +function sameSids(a: ReadonlyArray, b: ReadonlyArray): boolean { + return a.length === b.length && a.every((sid, index) => sid === b[index]); +} + +export class VoiceMeshEngine extends RTCEngine { + private readonly target: VoiceMeshTarget; + private readonly signal: VoiceMeshSignalClient; + private readonly senders: Readonly>; + private readonly peers = new Map(); + private readonly inbox = new Map>(); + private readonly reportedPeers = new Set(); + private lifecycle: 'new' | 'joined' | 'closed' = 'new'; + private room: Room | null = null; + private disposers: Array<() => void> = []; + private version = Date.now(); + private tracks: {v: number; tracks: Array} = {v: this.version, tracks: []}; + private conversionTimer: ReturnType | null = null; + private conversionHeld = false; + private speaking: SpeakingAnalyser | null = null; + + constructor(options: InternalRoomOptions, target: VoiceMeshTarget) { + super(options); + this.target = target; + this.signal = new VoiceMeshSignalClient(() => this.syncLocalInterest()); + this.client = this.signal; + this.senders = { + microphone: new VoiceMeshSender(this, 'microphone'), + camera: new VoiceMeshSender(this, 'camera'), + screen_share: new VoiceMeshSender(this, 'screen_share'), + screen_share_audio: new VoiceMeshSender(this, 'screen_share_audio'), + }; + this.pcManager = this.createTransportManager(); + } + + override get isClosed(): boolean { + return this.lifecycle !== 'joined'; + } + + override get isNewlyCreated(): boolean { + return this.lifecycle === 'new'; + } + + override get pendingReconnect(): boolean { + return false; + } + + override get serverVersion(): string | undefined { + return undefined; + } + + get iceServers(): Array> { + return this.target.iceServers; + } + + private get localParticipantSid(): string { + return `PA_${this.target.connectionId}`; + } + + override async join(_url: string, _token: string, opts: SignalOptions): ReturnType { + this.lifecycle = 'joined'; + this.signal.open(opts); + const joinResponse = new JoinResponse({ + participant: new ParticipantInfo({ + sid: this.localParticipantSid, + identity: buildVoiceParticipantIdentity(this.target.userId, this.target.connectionId), + state: ParticipantInfo_State.ACTIVE, + permission: new ParticipantPermission({canPublish: true, canSubscribe: true}), + }), + enabledPublishCodecs: MESH_VIDEO_CODEC_MIME_TYPES.map((mime) => new Codec({mime})), + }); + this.emit(EngineEvent.SignalConnected, joinResponse); + return {joinResponse, serverInfo: {version: VOICE_MESH_TOKEN}}; + } + + override async close(): Promise { + if (this.lifecycle === 'closed') return; + this.lifecycle = 'closed'; + this.emit(EngineEvent.Closing); + this.removeAllListeners(); + this.stopMesh(); + await this.signal.close(); + } + + start(room: Room): void { + if (this.lifecycle !== 'joined' || this.room) return; + activeEngine?.stopMesh(); + activeEngine = this; + this.room = room; + this.disposers = [ + subscribeVoiceMeshSignals(this.handleSignal), + voiceEngineV2AppVoiceStateAdapter.subscribe(this.syncRoster), + reaction( + () => GatewayConnection.isConnected, + (connected) => { + if (connected) this.handleGatewayReconnected(); + }, + ), + ]; + this.syncRoster(); + this.syncSpeaking(); + } + + override async createSender( + track: LocalTrack, + opts: TrackPublishOptions, + encodings?: Array, + ): Promise { + const sender = this.senders[this.sourceOf(track.source)]; + if (track instanceof LocalVideoTrack) track.codec = opts.videoCodec; + sender.attach(track.mediaStreamTrack, encodings); + return sender as unknown as RTCRtpSender; + } + + override async addTrack(req: AddTrackRequest): Promise { + const src = this.sourceOf(Track.sourceFromProto(req.source)); + const sid = voiceMeshTrackSid(this.target.connectionId, src); + this.senders[src].publish({sid, muted: req.muted, width: req.width, height: req.height}); + return new TrackInfo({ + sid, + type: req.type, + source: req.source, + name: req.name, + muted: req.muted, + width: req.width, + height: req.height, + stream: req.stream, + }); + } + + override removeTrack(sender: RTCRtpSender): boolean { + if (sender instanceof VoiceMeshSender) sender.unpublish(); + return false; + } + + override updateMuteStatus(trackSid: string, muted: boolean): void { + Object.values(this.senders) + .find((sender) => sender.sid === trackSid) + ?.setMuted(muted); + } + + override negotiate(): Promise { + return Promise.resolve(); + } + + override waitForPCInitialConnection(): Promise { + return Promise.resolve(); + } + + override verifyTransport(): boolean { + return !this.isClosed; + } + + override sendDataPacket(): Promise { + return Promise.resolve(); + } + + syncSender(src: VoiceMeshSource): Promise { + this.refreshTracks(); + if (src === 'microphone') this.syncSpeaking(); + return Promise.all(Array.from(this.peers.values(), (peer) => peer.requestSenderSync(src))).then(() => undefined); + } + + async collectSenderStats(src: VoiceMeshSource): Promise { + const reports = await Promise.all( + Array.from(this.peers.values()).flatMap((peer) => { + const rtpSender = peer.rtpSender(src); + if (!rtpSender) return []; + return [rtpSender.getStats().then((report) => [peer.connectionId, report] as const)]; + }), + ); + return mergeStatsReports(reports); + } + + sendPeerSignal(peer: VoiceMeshPeer, data: VoiceMeshSignalData): VoiceSignalSendResult { + if (!this.room) return VoiceSignalSendResult.Unsent; + const {guildId, channelId} = this.target; + return sendVoiceMeshSignal({guildId, channelId, to: peer.connectionId}, data); + } + + handlePeerStatus(peer: VoiceMeshPeer): void { + if (this.peers.get(peer.connectionId) !== peer) return; + const {status} = peer; + VoiceMeshPeers.setStatus(peer.connectionId, status); + if (status === 'connecting') return; + this.reportPeer(peer, status); + if (status !== 'connected') return; + for (const src of VOICE_MESH_SOURCES) void peer.requestSenderSync(src); + } + + private reportPeer(peer: VoiceMeshPeer, outcome: 'connected' | 'failed'): void { + if (this.reportedPeers.has(peer.connectionId)) return; + this.reportedPeers.add(peer.connectionId); + const report = { + channel_id: this.target.channelId, + guild_id: this.target.guildId, + participant_count: this.peers.size + 1, + outcome, + setup_ms: peer.setupMs, + ice_restarted: peer.iceRestarted, + }; + const stats = peer.getStats(); + if (!stats) { + queueVoiceMeshConnectionReport({...report, ...UNKNOWN_VOICE_MESH_CONNECTION_PATH}); + return; + } + void stats + .then(readVoiceMeshConnectionPath, () => UNKNOWN_VOICE_MESH_CONNECTION_PATH) + .then((path) => queueVoiceMeshConnectionReport({...report, ...path})); + } + + handlePeerTransport(peer: VoiceMeshPeer): void { + for (const src of VOICE_MESH_SOURCES) { + void peer.requestSenderSync(src); + this.syncPeerReceiver(peer, src); + } + if (peer.status === 'failed') return; + peer.send({t: 'tracks', ...this.tracks}); + if (peer.localWant) peer.send({t: 'want', ...peer.localWant}); + } + + handlePeerTracks(peer: VoiceMeshPeer): void { + if (this.peers.get(peer.connectionId) !== peer) return; + this.emitPeerInfo(peer, ParticipantInfo_State.ACTIVE); + this.syncPeerInterest(peer); + } + + handlePeerWant(): void { + this.syncAllSenders(); + } + + async syncPeerSender(peer: VoiceMeshPeer, src: VoiceMeshSource): Promise { + const rtpSender = peer.rtpSender(src); + if (!rtpSender) return; + const sender = this.senders[src]; + const track = sender.sid !== null && peer.remoteWant.has(sender.sid) ? sender.track : null; + try { + if (rtpSender.track !== track) await rtpSender.replaceTrack(track); + if (peer.rtpSender(src) !== rtpSender) return; + const parameters = rtpSender.getParameters(); + if (applySendParameters(parameters, sender.getParameters(), this.bitrateCap(src))) { + await rtpSender.setParameters(parameters); + } + } catch (error) { + logger.warn('Failed to sync mesh sender', {connectionId: peer.connectionId, src, error}); + } + } + + private syncPeerReceiver(peer: VoiceMeshPeer, src: VoiceMeshSource): void { + const participant = this.room?.remoteParticipants.get(peer.identity); + const info = peer.remoteTracks.find((track) => track.src === src); + if (!participant || !info) return; + const publication = participant.getTrackPublicationBySid(info.sid); + if (!publication) return; + const receiver = peer.rtpReceiver(src); + const serverMuted = + src === 'microphone' && + voiceEngineV2AppVoiceStateAdapter.getVoiceStateByConnectionId(peer.connectionId)?.mute === true; + if (receiver && receiver.track.readyState === 'live' && publication.isDesired && !serverMuted) { + if (publication.track?.mediaStreamTrack !== receiver.track) { + participant.addSubscribedMediaTrack(receiver.track, info.sid, new MediaStream([receiver.track]), receiver); + } + return; + } + if (!publication.track) return; + publication.track.stop(); + publication.setTrack(undefined); + } + + private readonly syncRoster = (): void => { + if (!this.room) return; + const {connectionId, channelId, guildId} = this.target; + const own = voiceEngineV2AppVoiceStateAdapter.getVoiceStateByConnectionId(connectionId); + const ownP2p = own?.channel_id === channelId ? own.p2p : undefined; + this.syncConversionWatchdog(ownP2p === false && !this.conversionHeld); + const roster = new Map(); + if (ownP2p === true) { + const states = voiceEngineV2AppVoiceStateAdapter.getAllVoiceStatesInChannel(guildId ?? ME, channelId); + for (const state of Object.values(states)) { + if (state.p2p === true && state.connection_id !== connectionId) roster.set(state.connection_id, state); + } + } + const before = this.peers.size; + for (const peer of Array.from(this.peers.values())) { + if (!roster.has(peer.connectionId)) this.removePeer(peer); + } + const kept = this.peers.size; + for (const state of roster.values()) { + if (!this.peers.has(state.connection_id)) this.addPeer(state); + } + if (kept !== before || this.peers.size !== kept) this.syncAllSenders(); + for (const peer of this.peers.values()) this.syncPeerReceiver(peer, 'microphone'); + }; + + private addPeer(state: NormalizedVoiceState): void { + const peer = new VoiceMeshPeer(this, state.connection_id, state.user_id, this.target.connectionId); + this.peers.set(peer.connectionId, peer); + VoiceMeshPeers.setStatus(peer.connectionId, peer.status); + this.emitPeerInfo(peer, ParticipantInfo_State.ACTIVE); + const pending = this.inbox.get(peer.connectionId) ?? []; + this.inbox.delete(peer.connectionId); + for (const signal of pending) { + if (signal.userId === peer.userId) peer.handleSignal(signal.data); + } + peer.start(); + } + + private removePeer(peer: VoiceMeshPeer): void { + peer.close(); + this.peers.delete(peer.connectionId); + this.inbox.delete(peer.connectionId); + VoiceMeshPeers.remove(peer.connectionId); + this.emitPeerInfo(peer, ParticipantInfo_State.DISCONNECTED); + } + + private emitPeerInfo(peer: VoiceMeshPeer, state: ParticipantInfo_State): void { + peer.infoVersion += 1; + const info = new ParticipantInfo({ + sid: peer.participantSid, + identity: peer.identity, + state, + version: peer.infoVersion, + permission: new ParticipantPermission({canPublish: true, canSubscribe: true}), + tracks: peer.remoteTracks.map( + (track) => + new TrackInfo({ + sid: track.sid, + type: Track.kindToProto(VOICE_MESH_TRACK_KINDS[track.src]), + source: Track.sourceToProto(VOICE_MESH_TRACK_SOURCES[track.src]), + muted: track.m, + width: track.w ?? 0, + height: track.h ?? 0, + }), + ), + }); + this.emit(EngineEvent.ParticipantUpdate, [info]); + } + + private readonly handleSignal = (signal: VoiceMeshIncomingSignal): void => { + if (!this.room || signal.channelId !== this.target.channelId || signal.guildId !== this.target.guildId) return; + const peer = this.peers.get(signal.from); + if (peer) { + if (signal.userId === peer.userId) peer.handleSignal(signal.data); + return; + } + if (signal.from === this.target.connectionId) return; + const pending = this.inbox.get(signal.from) ?? []; + if (pending.length >= INBOX_LIMIT) return; + pending.push(signal); + this.inbox.set(signal.from, pending); + }; + + private handleGatewayReconnected(): void { + for (const peer of this.peers.values()) { + peer.resume(); + peer.send({t: 'tracks', ...this.tracks}); + if (peer.localWant) peer.send({t: 'want', ...peer.localWant}); + } + } + + private syncLocalInterest(): void { + if (!this.room) return; + for (const peer of this.peers.values()) this.syncPeerInterest(peer); + } + + private syncPeerInterest(peer: VoiceMeshPeer): void { + const participant = this.room?.remoteParticipants.get(peer.identity); + const sids = participant + ? Array.from(participant.trackPublications.values()) + .filter((publication) => publication.isDesired && publication.isEnabled) + .map((publication) => publication.trackSid) + .sort() + : []; + if (!sameSids(peer.localWant?.sids ?? [], sids)) { + peer.localWant = {v: this.nextVersion(), sids}; + peer.send({t: 'want', ...peer.localWant}); + } + for (const src of VOICE_MESH_SOURCES) this.syncPeerReceiver(peer, src); + } + + private refreshTracks(): void { + const tracks = VOICE_MESH_SOURCES.flatMap((src) => { + const info = this.senders[src].info; + return info ? [info] : []; + }); + if (JSON.stringify(tracks) === JSON.stringify(this.tracks.tracks)) return; + this.tracks = {v: this.nextVersion(), tracks}; + for (const peer of this.peers.values()) peer.send({t: 'tracks', ...this.tracks}); + } + + private syncAllSenders(): void { + for (const peer of this.peers.values()) { + for (const src of VOICE_MESH_SOURCES) void peer.requestSenderSync(src); + } + } + + private bitrateCap(src: VoiceMeshSource): number | undefined { + switch (src) { + case 'screen_share': { + const sid = this.senders.screen_share.sid; + let watchers = 0; + for (const peer of this.peers.values()) { + if (sid !== null && peer.remoteWant.has(sid)) watchers += 1; + } + return Math.floor(SCREEN_SHARE_BUDGET_BPS / Math.max(1, watchers)); + } + case 'camera': + return Math.floor(CAMERA_BUDGET_BPS / Math.max(1, this.peers.size)); + default: + return undefined; + } + } + + holdConversion(held: boolean): void { + this.conversionHeld = held; + this.syncRoster(); + } + + private syncConversionWatchdog(converting: boolean): void { + if (!converting) { + this.clearConversionWatchdog(); + return; + } + if (this.conversionTimer) return; + this.conversionTimer = setTimeout(() => { + this.conversionTimer = null; + logger.warn('Voice state left P2P mode without a standard call grant', {channelId: this.target.channelId}); + this.emit(EngineEvent.Disconnected, DisconnectReason.STATE_MISMATCH); + void this.close(); + }, CONVERSION_GRACE_MS); + } + + private clearConversionWatchdog(): void { + if (!this.conversionTimer) return; + clearTimeout(this.conversionTimer); + this.conversionTimer = null; + } + + private syncSpeaking(): void { + const microphone = this.senders.microphone; + const track = this.room && microphone.sid !== null ? microphone.track : null; + if ((this.speaking?.track ?? null) === track) return; + this.stopSpeaking(); + if (!track) return; + const context = getSharedVoiceAudioContext(); + if (!context) return; + const source = context.createMediaStreamSource(new MediaStream([track])); + const analyser = context.createAnalyser(); + analyser.fftSize = 512; + source.connect(analyser); + const speaking: SpeakingAnalyser = { + track, + source, + analyser, + samples: new Float32Array(analyser.fftSize), + timer: setInterval(() => this.tickSpeaking(speaking), SPEAKING_TICK_MS), + lastLoudAt: null, + active: false, + }; + this.speaking = speaking; + } + + private tickSpeaking(speaking: SpeakingAnalyser): void { + speaking.analyser.getFloatTimeDomainData(speaking.samples); + const level = computeTimeDomainRms(speaking.samples); + const now = performance.now(); + if (level >= getRemoteSpeakingThresholdRms(VoiceSettings.getVadThreshold())) speaking.lastLoudAt = now; + const active = speaking.lastLoudAt !== null && now - speaking.lastLoudAt < SPEAKING_REMOTE_RELEASE_MS; + if (active === speaking.active) return; + speaking.active = active; + this.emitLocalSpeaking(level, active); + } + + private stopSpeaking(): void { + const speaking = this.speaking; + if (!speaking) return; + this.speaking = null; + clearInterval(speaking.timer); + speaking.source.disconnect(); + speaking.analyser.disconnect(); + if (speaking.active) this.emitLocalSpeaking(0, false); + } + + private emitLocalSpeaking(level: number, active: boolean): void { + this.emit(EngineEvent.SpeakersChanged, [new SpeakerInfo({sid: this.localParticipantSid, level, active})]); + } + + private stopMesh(): void { + if (!this.room) return; + this.room = null; + if (activeEngine === this) activeEngine = null; + for (const dispose of this.disposers) dispose(); + this.disposers = []; + this.clearConversionWatchdog(); + for (const peer of this.peers.values()) { + peer.close(); + VoiceMeshPeers.remove(peer.connectionId); + } + this.peers.clear(); + this.inbox.clear(); + this.stopSpeaking(); + flushVoiceMeshConnectionReports(); + } + + private sourceOf(source: Track.Source): VoiceMeshSource { + const src = VOICE_MESH_SOURCES.find((candidate) => VOICE_MESH_TRACK_SOURCES[candidate] === source); + if (!src) throw new TrackInvalidError(`P2P calls cannot publish a ${source} track`); + return src; + } + + private nextVersion(): number { + this.version += 1; + return this.version; + } + + private createTransportManager(): PCTransportManager { + const isClosed = () => this.isClosed; + return { + publisher: { + getStats: () => this.collectTransportStats(), + getTransceivers: () => Array.from(this.peers.values(), (peer) => peer.rtpTransceivers).flat(), + setTrackCodecBitrate: () => undefined, + removeTrack: () => undefined, + }, + mode: 'publisher-only', + get currentState() { + return isClosed() ? PCTransportState.CLOSED : PCTransportState.CONNECTED; + }, + } as unknown as PCTransportManager; + } + + private async collectTransportStats(): Promise { + const reports = await Promise.all( + Array.from(this.peers.values()).flatMap((peer) => { + const stats = peer.getStats(); + return stats ? [stats.then((report) => [peer.connectionId, report] as const)] : []; + }), + ); + return mergeStatsReports(reports); + } +} diff --git a/fluxer_app/src/features/voice/engine/mesh/VoiceMeshPeer.ts b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshPeer.ts new file mode 100644 index 000000000..2d3dcd456 --- /dev/null +++ b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshPeer.ts @@ -0,0 +1,422 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {VoiceSignalSendResult} from '@app/features/gateway/transport/GatewaySocket'; +import {Logger} from '@app/features/platform/utils/AppLogger'; +import type {VoiceMeshEngine} from '@app/features/voice/engine/mesh/VoiceMeshEngine'; +import type {VoiceMeshPeerStatus} from '@app/features/voice/engine/mesh/VoiceMeshPeers'; +import {VOICE_MESH_TRACK_KINDS} from '@app/features/voice/engine/mesh/VoiceMeshSender'; +import { + VOICE_MESH_SOURCES, + type VoiceMeshSignalData, + type VoiceMeshSource, + type VoiceMeshTrackInfo, + voiceMeshTrackSid, +} from '@app/features/voice/engine/mesh/VoiceMeshSignal'; +import {buildVoiceParticipantIdentity} from '@app/features/voice/utils/VoiceParticipantIdentity'; +import {selectPublisherCodecPreferences, Track} from 'livekit-client'; + +const logger = new Logger('VoiceMeshPeer'); + +const SETUP_DEADLINE_MS = 15_000; +const DISCONNECTED_GRACE_MS = 5_000; +const RESTART_DEADLINE_MS = 10_000; + +type VoiceMeshNegotiationSignal = Exclude; + +function withMimeType(mimeType: string): (codec: RTCRtpCodec) => boolean { + return (codec) => codec.mimeType.toLowerCase() === mimeType; +} + +function codecPreferences(src: VoiceMeshSource): Array { + const kind = VOICE_MESH_TRACK_KINDS[src]; + const codecs = RTCRtpReceiver.getCapabilities(kind)?.codecs ?? []; + if (kind === Track.Kind.Audio) { + return [...codecs.filter(withMimeType('audio/red')), ...codecs.filter(withMimeType('audio/opus'))]; + } + return [ + ...selectPublisherCodecPreferences('h264', codecs).filter(withMimeType('video/h264')), + ...codecs.filter(withMimeType('video/vp8')), + ...codecs.filter(withMimeType('video/rtx')), + ]; +} + +export class VoiceMeshPeer { + readonly participantSid: string; + readonly identity: string; + readonly isOfferer: boolean; + status: VoiceMeshPeerStatus = 'connecting'; + remoteTracks: ReadonlyArray = []; + remoteWant: ReadonlySet = new Set(); + localWant: {v: number; sids: Array} | null = null; + infoVersion = 0; + setupMs: number | null = null; + iceRestarted = false; + private readonly createdAt = performance.now(); + private remoteTracksVersion = 0; + private remoteWantVersion = 0; + private pc: RTCPeerConnection | null = null; + private transceivers: ReadonlyArray = []; + private generation = 0; + private answered = false; + private restarting = false; + private closed = false; + private queue: Promise = Promise.resolve(); + private readonly pendingSenderSyncs = new Map>(); + private setupTimer: ReturnType | null = null; + private disconnectedTimer: ReturnType | null = null; + private restartTimer: ReturnType | null = null; + + constructor( + private readonly engine: VoiceMeshEngine, + readonly connectionId: string, + readonly userId: string, + localConnectionId: string, + ) { + this.participantSid = `PA_${connectionId}`; + this.identity = buildVoiceParticipantIdentity(userId, connectionId); + this.isOfferer = localConnectionId < connectionId; + } + + get rtpTransceivers(): ReadonlyArray { + return this.transceivers; + } + + rtpSender(src: VoiceMeshSource): RTCRtpSender | null { + return this.transceivers[VOICE_MESH_SOURCES.indexOf(src)]?.sender ?? null; + } + + rtpReceiver(src: VoiceMeshSource): RTCRtpReceiver | null { + return this.transceivers[VOICE_MESH_SOURCES.indexOf(src)]?.receiver ?? null; + } + + getStats(): Promise | null { + return this.pc?.getStats() ?? null; + } + + start(): void { + this.armSetupDeadline(); + void this.enqueue(async () => { + if (this.isOfferer) { + if (!this.pc) await this.startGeneration(); + return; + } + if (this.generation === 0) this.send({t: 'need-offer', g: 0}); + }); + } + + resume(): void { + if (this.closed || this.status === 'failed') return; + if (this.status === 'connected') { + if (this.restarting) void this.enqueue(() => this.recover()); + return; + } + if (this.isOfferer) { + void this.enqueue(() => this.startGeneration()); + return; + } + this.armSetupDeadline(); + this.send({t: 'need-offer', g: 0}); + } + + handleSignal(data: VoiceMeshSignalData): void { + if (this.closed || this.status === 'failed') return; + switch (data.t) { + case 'tracks': + this.receiveTracks(data.v, data.tracks); + return; + case 'want': + this.receiveWant(data.v, data.sids); + return; + default: + void this.enqueue(() => this.negotiate(data)); + } + } + + send(data: VoiceMeshSignalData): void { + if (this.closed || this.status === 'failed') return; + if (this.engine.sendPeerSignal(this, data) !== VoiceSignalSendResult.Oversized) return; + logger.warn('Voice signal exceeds the gateway frame cap', {connectionId: this.connectionId, type: data.t}); + this.fail(); + } + + requestSenderSync(src: VoiceMeshSource): Promise { + const pending = this.pendingSenderSyncs.get(src); + if (pending) return pending; + const run = this.enqueue(async () => { + this.pendingSenderSyncs.delete(src); + await this.engine.syncPeerSender(this, src); + }); + this.pendingSenderSyncs.set(src, run); + return run; + } + + close(): void { + if (this.closed) return; + this.closed = true; + this.resetTransport(); + this.pendingSenderSyncs.clear(); + } + + private receiveTracks(v: number, tracks: ReadonlyArray): void { + if (v <= this.remoteTracksVersion) return; + const sources = new Set(); + for (const track of tracks) { + if (sources.has(track.src) || track.sid !== voiceMeshTrackSid(this.connectionId, track.src)) return; + sources.add(track.src); + } + this.remoteTracksVersion = v; + this.remoteTracks = tracks; + this.engine.handlePeerTracks(this); + } + + private receiveWant(v: number, sids: ReadonlyArray): void { + if (v <= this.remoteWantVersion) return; + this.remoteWantVersion = v; + this.remoteWant = new Set(sids); + this.engine.handlePeerWant(); + } + + private async negotiate(data: VoiceMeshNegotiationSignal): Promise { + if (this.status === 'failed') return; + switch (data.t) { + case 'offer': + if (this.isOfferer || data.g < this.generation) return; + if (data.g > this.generation) { + await this.acceptGeneration(data.g, data.sdp); + return; + } + await this.answerRestart(data.sdp); + return; + case 'answer': { + const pc = this.pc; + if (!this.isOfferer || !pc || data.g !== this.generation || pc.signalingState !== 'have-local-offer') return; + await pc.setRemoteDescription({type: 'answer', sdp: data.sdp}); + if (this.isStale(pc, data.g)) return; + this.answered = true; + return; + } + case 'ice': { + const pc = this.pc; + if (!pc || data.g !== this.generation) return; + try { + await pc.addIceCandidate(data.c); + } catch (error) { + logger.debug('Dropping ICE candidate', {connectionId: this.connectionId, error}); + } + return; + } + case 'need-offer': + if (!this.isOfferer) return; + if (data.restart) { + if (data.g === this.generation) await this.recover(); + return; + } + if (data.g === this.generation && this.answered) return; + await this.startGeneration(); + } + } + + private async startGeneration(): Promise { + const g = this.generation === 0 ? Date.now() : this.generation + 1; + this.beginGeneration(g); + const pc = this.createTransport(g); + this.transceivers = VOICE_MESH_SOURCES.map((src) => { + const transceiver = pc.addTransceiver(VOICE_MESH_TRACK_KINDS[src], {direction: 'sendrecv'}); + transceiver.setCodecPreferences(codecPreferences(src)); + return transceiver; + }); + this.engine.handlePeerTransport(this); + await this.sendOffer(pc, g, false); + } + + private async acceptGeneration(g: number, sdp: string): Promise { + this.beginGeneration(g); + const pc = this.createTransport(g); + await pc.setRemoteDescription({type: 'offer', sdp}); + if (this.isStale(pc, g)) return; + const transceivers = pc.getTransceivers(); + const matchesLayout = + transceivers.length === VOICE_MESH_SOURCES.length && + VOICE_MESH_SOURCES.every((src, index) => transceivers[index].receiver.track.kind === VOICE_MESH_TRACK_KINDS[src]); + if (!matchesLayout) { + logger.warn('Offer does not carry the mesh transceiver layout', {connectionId: this.connectionId}); + this.fail(); + return; + } + VOICE_MESH_SOURCES.forEach((src, index) => { + transceivers[index].direction = 'sendrecv'; + transceivers[index].setCodecPreferences(codecPreferences(src)); + }); + this.transceivers = transceivers; + this.engine.handlePeerTransport(this); + await this.sendAnswer(pc, g); + } + + private async answerRestart(sdp: string): Promise { + const pc = this.pc; + if (!pc) return; + const g = this.generation; + this.iceRestarted = true; + await pc.setRemoteDescription({type: 'offer', sdp}); + if (this.isStale(pc, g)) return; + await this.sendAnswer(pc, g); + } + + private async sendOffer(pc: RTCPeerConnection, g: number, iceRestart: boolean): Promise { + const offer = await pc.createOffer({iceRestart}); + if (this.isStale(pc, g)) return; + await pc.setLocalDescription(offer); + if (this.isStale(pc, g)) return; + this.send({t: 'offer', g, sdp: offer.sdp ?? ''}); + } + + private async sendAnswer(pc: RTCPeerConnection, g: number): Promise { + const answer = await pc.createAnswer(); + if (this.isStale(pc, g)) return; + await pc.setLocalDescription(answer); + if (this.isStale(pc, g)) return; + this.send({t: 'answer', g, sdp: answer.sdp ?? ''}); + } + + private async recover(): Promise { + const pc = this.pc; + if (!pc || this.status === 'failed') return; + if (this.restarting) { + this.resendRestart(pc); + return; + } + this.restarting = true; + this.iceRestarted = true; + this.restartTimer = setTimeout(() => { + this.restartTimer = null; + if (pc.connectionState !== 'connected') { + this.fail(); + return; + } + this.restarting = false; + }, RESTART_DEADLINE_MS); + if (this.isOfferer) { + await this.sendOffer(pc, this.generation, true); + return; + } + this.send({t: 'need-offer', g: this.generation, restart: true}); + } + + private resendRestart(pc: RTCPeerConnection): void { + if (!this.isOfferer) { + this.send({t: 'need-offer', g: this.generation, restart: true}); + return; + } + const offer = pc.localDescription; + if (pc.signalingState !== 'have-local-offer' || !offer) return; + this.send({t: 'offer', g: this.generation, sdp: offer.sdp}); + } + + private beginGeneration(g: number): void { + this.resetTransport(); + this.generation = g; + this.answered = false; + this.restarting = false; + this.setStatus('connecting'); + this.armSetupDeadline(); + } + + private createTransport(g: number): RTCPeerConnection { + const pc = new RTCPeerConnection({iceServers: this.engine.iceServers, bundlePolicy: 'max-bundle'}); + pc.onicecandidate = (event) => { + if (this.pc !== pc) return; + this.send({t: 'ice', g, c: event.candidate ? event.candidate.toJSON() : null}); + }; + pc.onconnectionstatechange = () => { + if (this.pc !== pc) return; + this.handleConnectionState(pc.connectionState); + }; + this.pc = pc; + return pc; + } + + private handleConnectionState(state: RTCPeerConnectionState): void { + switch (state) { + case 'connected': + this.clearTimers(); + this.restarting = false; + this.setupMs ??= Math.round(performance.now() - this.createdAt); + this.setStatus('connected'); + return; + case 'disconnected': + this.disconnectedTimer ??= setTimeout(() => { + this.disconnectedTimer = null; + void this.enqueue(() => this.recover()); + }, DISCONNECTED_GRACE_MS); + return; + case 'failed': + if (this.restarting) { + this.fail(); + return; + } + void this.enqueue(() => this.recover()); + return; + default: + return; + } + } + + private fail(): void { + if (this.closed || this.status === 'failed') return; + this.resetTransport(); + this.setStatus('failed'); + this.engine.handlePeerTransport(this); + } + + private setStatus(status: VoiceMeshPeerStatus): void { + if (this.status === status) return; + this.status = status; + this.engine.handlePeerStatus(this); + } + + private armSetupDeadline(): void { + if (this.setupTimer) clearTimeout(this.setupTimer); + this.setupTimer = setTimeout(() => { + this.setupTimer = null; + this.fail(); + }, SETUP_DEADLINE_MS); + } + + private clearTimers(): void { + for (const timer of [this.setupTimer, this.disconnectedTimer, this.restartTimer]) { + if (timer) clearTimeout(timer); + } + this.setupTimer = null; + this.disconnectedTimer = null; + this.restartTimer = null; + } + + private resetTransport(): void { + this.clearTimers(); + const pc = this.pc; + this.pc = null; + this.transceivers = []; + if (!pc) return; + pc.onicecandidate = null; + pc.onconnectionstatechange = null; + pc.close(); + } + + private isStale(pc: RTCPeerConnection, g: number): boolean { + return this.closed || this.pc !== pc || this.generation !== g; + } + + private enqueue(task: () => Promise | void): Promise { + this.queue = this.queue + .then(async () => { + if (this.closed) return; + await task(); + }) + .catch((error: unknown) => { + if (this.closed) return; + logger.warn('Mesh negotiation failed', {connectionId: this.connectionId, error}); + this.fail(); + }); + return this.queue; + } +} diff --git a/fluxer_app/src/features/voice/engine/mesh/VoiceMeshPeers.ts b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshPeers.ts new file mode 100644 index 000000000..6d2d6820f --- /dev/null +++ b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshPeers.ts @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {makeAutoObservable} from 'mobx'; + +export type VoiceMeshPeerStatus = 'connecting' | 'connected' | 'failed'; + +class VoiceMeshPeers { + statuses: Readonly> = {}; + + constructor() { + makeAutoObservable(this, {}, {autoBind: true}); + } + + get failedConnectionIds(): ReadonlyArray { + return Object.keys(this.statuses).filter((connectionId) => this.statuses[connectionId] === 'failed'); + } + + get hasFailedPeer(): boolean { + return this.failedConnectionIds.length > 0; + } + + getStatus(connectionId: string): VoiceMeshPeerStatus | null { + return this.statuses[connectionId] ?? null; + } + + setStatus(connectionId: string, status: VoiceMeshPeerStatus): void { + if (this.statuses[connectionId] === status) return; + this.statuses = {...this.statuses, [connectionId]: status}; + } + + remove(connectionId: string): void { + if (!Object.hasOwn(this.statuses, connectionId)) return; + const {[connectionId]: _removed, ...rest} = this.statuses; + this.statuses = rest; + } + + reset(): void { + this.statuses = {}; + } +} + +export default new VoiceMeshPeers(); diff --git a/fluxer_app/src/features/voice/engine/mesh/VoiceMeshRoom.ts b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshRoom.ts new file mode 100644 index 000000000..2a98152ef --- /dev/null +++ b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshRoom.ts @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {VoiceMeshEngine, type VoiceMeshTarget} from '@app/features/voice/engine/mesh/VoiceMeshEngine'; +import {Room, RoomEvent, type RoomOptions} from 'livekit-client'; + +const meshRooms = new WeakSet(); + +export function createVoiceMeshRoom(roomOptions: RoomOptions, target: VoiceMeshTarget): Room { + const room = new Room({ + ...roomOptions, + dynacast: false, + e2ee: undefined, + encryption: undefined, + publishDefaults: {...roomOptions.publishDefaults, simulcast: false, backupCodec: false}, + createEngine: (options) => new VoiceMeshEngine(options, target), + }); + room.on(RoomEvent.Connected, () => { + if (room.engine instanceof VoiceMeshEngine) room.engine.start(room); + }); + meshRooms.add(room); + return room; +} + +export function isVoiceMeshRoom(room: Room): boolean { + return meshRooms.has(room); +} + +export function holdVoiceMeshConversion(room: Room | null, held: boolean): void { + if (room?.engine instanceof VoiceMeshEngine) room.engine.holdConversion(held); +} diff --git a/fluxer_app/src/features/voice/engine/mesh/VoiceMeshSender.ts b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshSender.ts new file mode 100644 index 000000000..e39499979 --- /dev/null +++ b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshSender.ts @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import type {VoiceMeshEngine} from '@app/features/voice/engine/mesh/VoiceMeshEngine'; +import type {VoiceMeshSource, VoiceMeshTrackInfo} from '@app/features/voice/engine/mesh/VoiceMeshSignal'; +import {Track} from 'livekit-client'; + +export const VOICE_MESH_TRACK_SOURCES: Readonly> = { + microphone: Track.Source.Microphone, + camera: Track.Source.Camera, + screen_share: Track.Source.ScreenShare, + screen_share_audio: Track.Source.ScreenShareAudio, +}; + +export const VOICE_MESH_TRACK_KINDS: Readonly> = { + microphone: Track.Kind.Audio, + camera: Track.Kind.Video, + screen_share: Track.Kind.Video, + screen_share_audio: Track.Kind.Audio, +}; + +interface VoiceMeshPublication { + sid: string; + muted: boolean; + width: number; + height: number; +} + +function createSendParameters( + encodings: ReadonlyArray, + degradationPreference?: RTCDegradationPreference, +): RTCRtpSendParameters { + return { + transactionId: '', + encodings: encodings.map((encoding) => ({...encoding})), + codecs: [], + headerExtensions: [], + rtcp: {}, + ...(degradationPreference !== undefined && {degradationPreference}), + }; +} + +export class VoiceMeshSender { + readonly transport: RTCDtlsTransport | null = null; + track: MediaStreamTrack | null = null; + private publication: VoiceMeshPublication | null = null; + private parameters: RTCRtpSendParameters = createSendParameters([{}]); + + constructor( + private readonly engine: VoiceMeshEngine, + readonly src: VoiceMeshSource, + ) {} + + get sid(): string | null { + return this.publication?.sid ?? null; + } + + get info(): VoiceMeshTrackInfo | null { + const publication = this.publication; + if (!publication) return null; + return { + src: this.src, + sid: publication.sid, + m: publication.muted, + ...(publication.width > 0 && {w: publication.width}), + ...(publication.height > 0 && {h: publication.height}), + }; + } + + attach(track: MediaStreamTrack, encodings: ReadonlyArray | undefined): void { + this.track = track; + this.parameters = createSendParameters(encodings?.length ? encodings : [{}]); + void this.engine.syncSender(this.src); + } + + publish(publication: VoiceMeshPublication): void { + this.publication = publication; + void this.engine.syncSender(this.src); + } + + setMuted(muted: boolean): void { + if (!this.publication || this.publication.muted === muted) return; + this.publication = {...this.publication, muted}; + void this.engine.syncSender(this.src); + } + + unpublish(): void { + this.publication = null; + this.track = null; + void this.engine.syncSender(this.src); + } + + replaceTrack(track: MediaStreamTrack | null): Promise { + this.track = track; + return this.engine.syncSender(this.src); + } + + getParameters(): RTCRtpSendParameters { + return createSendParameters(this.parameters.encodings, this.parameters.degradationPreference); + } + + setParameters(parameters: RTCRtpSendParameters): Promise { + this.parameters = createSendParameters(parameters.encodings, parameters.degradationPreference); + return this.engine.syncSender(this.src); + } + + getStats(): Promise { + return this.engine.collectSenderStats(this.src); + } +} diff --git a/fluxer_app/src/features/voice/engine/mesh/VoiceMeshSignal.ts b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshSignal.ts new file mode 100644 index 000000000..a8a4e7c22 --- /dev/null +++ b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshSignal.ts @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import GatewayConnection from '@app/features/gateway/transport/GatewayConnection'; +import {VoiceSignalSendResult} from '@app/features/gateway/transport/GatewaySocket'; + +export const VOICE_MESH_TOKEN = 'p2p'; +export const VOICE_MESH_ENDPOINT = 'wss://p2p.invalid'; + +export const VOICE_MESH_SOURCES = ['microphone', 'camera', 'screen_share', 'screen_share_audio'] as const; + +export type VoiceMeshSource = (typeof VOICE_MESH_SOURCES)[number]; + +export type VoiceMeshTrackInfo = { + src: VoiceMeshSource; + sid: string; + m: boolean; + w?: number; + h?: number; +}; + +export type VoiceMeshSignalData = + | {t: 'offer'; g: number; sdp: string} + | {t: 'answer'; g: number; sdp: string} + | {t: 'ice'; g: number; c: RTCIceCandidateInit | null} + | {t: 'need-offer'; g: number; restart?: true} + | {t: 'tracks'; v: number; tracks: Array} + | {t: 'want'; v: number; sids: Array}; + +export interface VoiceMeshSignalTarget { + guildId: string | null; + channelId: string; + to: string; +} + +export interface VoiceMeshIncomingSignal { + guildId: string | null; + channelId: string; + from: string; + userId: string; + data: VoiceMeshSignalData; +} + +type VoiceMeshSignalListener = (signal: VoiceMeshIncomingSignal) => void; + +const listeners = new Set(); + +export function voiceMeshTrackSid(connectionId: string, src: VoiceMeshSource): string { + return `TR_${connectionId}_${src}`; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isCounter(value: unknown): value is number { + return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; +} + +function isOptionalDimension(value: unknown): value is number | undefined { + return value === undefined || (typeof value === 'number' && Number.isFinite(value) && value >= 0); +} + +function isVoiceMeshSource(value: unknown): value is VoiceMeshSource { + return VOICE_MESH_SOURCES.includes(value as VoiceMeshSource); +} + +function isOptionalString(value: unknown): value is string | null | undefined { + return value === undefined || value === null || typeof value === 'string'; +} + +function isOptionalIndex(value: unknown): value is number | null | undefined { + return value === undefined || value === null || isCounter(value); +} + +function parseCandidate(value: unknown): RTCIceCandidateInit | null | undefined { + if (value === null) return null; + if (!isRecord(value) || typeof value.candidate !== 'string') return undefined; + const {candidate, sdpMid, sdpMLineIndex, usernameFragment} = value; + if (!isOptionalString(sdpMid) || !isOptionalString(usernameFragment) || !isOptionalIndex(sdpMLineIndex)) { + return undefined; + } + return {candidate, sdpMid, sdpMLineIndex, usernameFragment}; +} + +function parseTrack(value: unknown): VoiceMeshTrackInfo | null { + if (!isRecord(value)) return null; + const {src, sid, m, w, h} = value; + if (!isVoiceMeshSource(src) || typeof sid !== 'string' || typeof m !== 'boolean') return null; + if (!isOptionalDimension(w) || !isOptionalDimension(h)) return null; + return {src, sid, m, ...(w !== undefined && {w}), ...(h !== undefined && {h})}; +} + +export function parseVoiceMeshSignalData(value: unknown): VoiceMeshSignalData | null { + if (!isRecord(value)) return null; + switch (value.t) { + case 'offer': + case 'answer': + if (!isCounter(value.g) || typeof value.sdp !== 'string') return null; + return {t: value.t, g: value.g, sdp: value.sdp}; + case 'ice': { + if (!isCounter(value.g)) return null; + const c = parseCandidate(value.c); + if (c === undefined) return null; + return {t: 'ice', g: value.g, c}; + } + case 'need-offer': + if (!isCounter(value.g)) return null; + if (value.restart !== undefined && value.restart !== true) return null; + return {t: 'need-offer', g: value.g, ...(value.restart === true && {restart: true})}; + case 'tracks': { + if (!isCounter(value.v) || !Array.isArray(value.tracks)) return null; + const tracks: Array = []; + for (const entry of value.tracks) { + const track = parseTrack(entry); + if (!track) return null; + tracks.push(track); + } + return {t: 'tracks', v: value.v, tracks}; + } + case 'want': { + if (!isCounter(value.v) || !Array.isArray(value.sids)) return null; + const sids: Array = value.sids; + if (!sids.every((sid): sid is string => typeof sid === 'string')) return null; + return {t: 'want', v: value.v, sids}; + } + default: + return null; + } +} + +export function sendVoiceMeshSignal(target: VoiceMeshSignalTarget, data: VoiceMeshSignalData): VoiceSignalSendResult { + const socket = GatewayConnection.socket; + if (!socket) return VoiceSignalSendResult.Unsent; + return socket.sendVoiceSignal({guild_id: target.guildId, channel_id: target.channelId, to: target.to, data}); +} + +export function subscribeVoiceMeshSignals(listener: VoiceMeshSignalListener): () => void { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; +} + +export function dispatchVoiceMeshSignal(signal: VoiceMeshIncomingSignal): void { + for (const listener of listeners) { + listener(signal); + } +} diff --git a/fluxer_app/src/features/voice/engine/mesh/VoiceMeshSignalClient.ts b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshSignalClient.ts new file mode 100644 index 000000000..7bced6c61 --- /dev/null +++ b/fluxer_app/src/features/voice/engine/mesh/VoiceMeshSignalClient.ts @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {SignalClient, SignalConnectionState, type SignalOptions} from 'livekit-client'; + +export class VoiceMeshSignalClient extends SignalClient { + private connected = false; + + constructor(private readonly onInterestChanged: () => void) { + super(); + } + + override get currentState(): SignalConnectionState { + return this.connected ? SignalConnectionState.CONNECTED : SignalConnectionState.DISCONNECTED; + } + + open(options: SignalOptions): void { + this.connectOptions = options; + this.connected = true; + } + + override close(): Promise { + this.connected = false; + return Promise.resolve(); + } + + override sendUpdateSubscription(): Promise { + this.onInterestChanged(); + return Promise.resolve(); + } + + override sendUpdateTrackSettings(): void { + this.onInterestChanged(); + } + + override sendMuteTrack(): Promise { + return Promise.resolve(); + } + + override sendUpdateLocalAudioTrack(): Promise { + return Promise.resolve(); + } + + override sendUpdateSubscriptionPermissions(): Promise { + return Promise.resolve(); + } + + override sendUpdateVideoLayers(): Promise { + return Promise.resolve(); + } + + override sendLeave(): Promise { + return Promise.resolve(); + } +} diff --git a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppConnectionHostAdapter.ts b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppConnectionHostAdapter.ts index ec6a997c4..84b9b5e56 100644 --- a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppConnectionHostAdapter.ts +++ b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppConnectionHostAdapter.ts @@ -1,7 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import assert from 'node:assert/strict'; +import Authentication from '@app/features/auth/state/Authentication'; import {Logger} from '@app/features/platform/utils/AppLogger'; +import {createVoiceMeshRoom, holdVoiceMeshConversion} from '@app/features/voice/engine/mesh/VoiceMeshRoom'; import {Store} from '@app/features/voice/engine/Store'; import {sendVoiceStateDisconnect} from '@app/features/voice/engine/VoiceChannelConnector'; import { @@ -72,6 +74,7 @@ export interface VoiceServerUpdateData { guild_id?: string; channel_id?: string; e2ee_key?: string | null; + ice_servers?: Array>; } export interface VoiceConnectionState { @@ -186,6 +189,12 @@ function createRoomOptions( return {roomOptions, e2eeKeyProvider, e2eeWorker}; } +function requireCurrentUserId(): string { + const userId = Authentication.currentUserId; + assertNonEmptyString(userId, 'mesh room current user id'); + return userId; +} + function createRoomConnectOptions(): RoomConnectOptions { const connectOptions: RoomConnectOptions = { autoSubscribe: false, @@ -518,6 +527,7 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store { if (previousRoom) { shouldStopPreviousRoomTracks = isChannelMove || onBeforeReconnect?.(isChannelMove, previousRoom) !== true; previousRoom.removeAllListeners(); + holdVoiceMeshConversion(previousRoom, true); if (isChannelMove) { this.disconnectPreviousRoom(previousRoom); previousRoom = null; @@ -546,7 +556,15 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store { return; } const {roomOptions, e2eeKeyProvider, e2eeWorker} = createRoomOptions(e2eeKey, subscriberVideoCodecExclusions); - const room = new LiveKitRoom(roomOptions); + const room = raw.ice_servers + ? createVoiceMeshRoom(roomOptions, { + iceServers: raw.ice_servers, + guildId, + channelId: resolvedChannelId, + connectionId: raw.connection_id, + userId: requireCurrentUserId(), + }) + : new LiveKitRoom(roomOptions); ownE2EEWorker(room, e2eeWorker); let roomClosed = false; const closeRoom = () => { @@ -695,6 +713,7 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store { this.update(() => { this.transitionConnection({type: 'hotSwap.start', pendingRoom: newRoom, previousRoom: existingRoom}); }); + holdVoiceMeshConversion(existingRoom, true); this.clearHotSwapTimeout(); this.hotSwapTimeoutSub = timer(REGION_HOT_SWAP_TIMEOUT_MS).subscribe(() => { if (this.hotSwapState.inProgress && this.hotSwapState.pendingRoom === newRoom) { @@ -790,6 +809,7 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store { this.update(() => { this.transitionConnection({type: 'hotSwap.reset'}); }); + holdVoiceMeshConversion(existingRoom, false); this.clearHotSwapTimeout(); this.clearHotSwapQueue(); logger.info('Region hot-swap: keeping old room on previous endpoint'); @@ -867,7 +887,7 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store { assert.ok(this.hotSwapOperationQueue.length <= 4096, 'abortHotSwap pre-condition: queue under cap'); assert.ok(this.connectionSnapshot !== null, 'abortHotSwap pre-condition: connection snapshot present'); if (!this.hotSwapState.inProgress) return; - const {pendingRoom} = this.hotSwapState; + const {pendingRoom, previousRoom} = this.hotSwapState; logger.info('Aborting region hot-swap'); if (pendingRoom) { try { @@ -880,6 +900,7 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store { this.update(() => { this.transitionConnection({type: 'hotSwap.abort'}); }); + holdVoiceMeshConversion(previousRoom, false); this.clearHotSwapTimeout(); this.clearHotSwapQueue(); } diff --git a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppRemoteSpeakingAdapter.ts b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppRemoteSpeakingAdapter.ts index 6e5534658..da2d9fc85 100644 --- a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppRemoteSpeakingAdapter.ts +++ b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppRemoteSpeakingAdapter.ts @@ -34,7 +34,7 @@ const logger = new Logger('VoiceEngineV2AppRemoteSpeakingAdapter'); const REMOTE_SPEAKING_ANALYSER_INTERVAL_MS = 50; const REMOTE_SPEAKING_ANALYSER_HANDLES_CAP = 256; -function computeTimeDomainRms(samples: Float32Array): number { +export function computeTimeDomainRms(samples: Float32Array): number { if (samples.length === 0) return 0; let sumSquares = 0; for (let i = 0; i < samples.length; i++) { diff --git a/fluxer_app/src/features/voice/events/VoiceServerUpdate.ts b/fluxer_app/src/features/voice/events/VoiceServerUpdate.ts index 830dc6666..359621fce 100644 --- a/fluxer_app/src/features/voice/events/VoiceServerUpdate.ts +++ b/fluxer_app/src/features/voice/events/VoiceServerUpdate.ts @@ -2,8 +2,10 @@ import type {GatewayHandlerContext} from '@app/features/gateway/events/EventRouter'; import MediaEngine from '@app/features/voice/engine/MediaEngineFacade'; +import {VOICE_MESH_ENDPOINT, VOICE_MESH_TOKEN} from '@app/features/voice/engine/mesh/VoiceMeshSignal'; -interface VoiceServerUpdatePayload { +interface VoiceServerUpdateSfuPayload { + p2p?: undefined; token: string; endpoint: string; connection_id: string; @@ -12,7 +14,29 @@ interface VoiceServerUpdatePayload { e2ee_key?: string | null; } +interface VoiceServerUpdateP2pPayload { + p2p: true; + ice_servers: Array>; + connection_id: string; + guild_id?: string; + channel_id?: string; +} + +type VoiceServerUpdatePayload = VoiceServerUpdateSfuPayload | VoiceServerUpdateP2pPayload; + export function handleVoiceServerUpdate(data: VoiceServerUpdatePayload, _context: GatewayHandlerContext): void { + if (data.p2p === true) { + MediaEngine.handleVoiceServerUpdate({ + token: VOICE_MESH_TOKEN, + endpoint: VOICE_MESH_ENDPOINT, + connection_id: data.connection_id, + guild_id: data.guild_id, + channel_id: data.channel_id, + e2ee_key: null, + ice_servers: data.ice_servers, + }); + return; + } MediaEngine.handleVoiceServerUpdate({ token: data.token, endpoint: data.endpoint, diff --git a/fluxer_app/src/features/voice/events/VoiceSignal.ts b/fluxer_app/src/features/voice/events/VoiceSignal.ts new file mode 100644 index 000000000..7f5c0afc6 --- /dev/null +++ b/fluxer_app/src/features/voice/events/VoiceSignal.ts @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import type {GatewayHandlerContext} from '@app/features/gateway/events/EventRouter'; +import {Logger} from '@app/features/platform/utils/AppLogger'; +import {dispatchVoiceMeshSignal, parseVoiceMeshSignalData} from '@app/features/voice/engine/mesh/VoiceMeshSignal'; + +const logger = new Logger('VoiceSignal'); + +interface VoiceSignalPayload { + guild_id?: string; + channel_id: string; + from: string; + user_id: string; + data: unknown; +} + +export function handleVoiceSignal(data: VoiceSignalPayload, _context: GatewayHandlerContext): void { + const signal = parseVoiceMeshSignalData(data.data); + if (!signal) { + logger.warn('Dropping malformed voice signal', {from: data.from}); + return; + } + dispatchVoiceMeshSignal({ + guildId: data.guild_id ?? null, + channelId: data.channel_id, + from: data.from, + userId: data.user_id, + data: signal, + }); +} diff --git a/fluxer_app/src/features/voice/state/ChannelP2pStatus.ts b/fluxer_app/src/features/voice/state/ChannelP2pStatus.ts new file mode 100644 index 000000000..842ff5a54 --- /dev/null +++ b/fluxer_app/src/features/voice/state/ChannelP2pStatus.ts @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {ME} from '@fluxer/constants/src/AppConstants'; + +export function isChannelP2p(guildId: string | null, channelId: string): boolean { + const channelStates = globalThis.window?._mediaEngine?.getAllVoiceStatesInGuild(guildId ?? ME)?.[channelId]; + if (!channelStates) return false; + const states = Object.values(channelStates); + return states.length > 0 && states.every((state) => state.p2p === true); +} + +export function isActiveVoiceChannelP2p(): boolean { + const mediaEngine = globalThis.window?._mediaEngine; + const channelId = mediaEngine?.channelId; + if (!mediaEngine || !channelId) return false; + return isChannelP2p(mediaEngine.guildId, channelId); +} diff --git a/fluxer_app/src/features/voice/state/VoiceP2pConsent.ts b/fluxer_app/src/features/voice/state/VoiceP2pConsent.ts new file mode 100644 index 000000000..ab8a87df8 --- /dev/null +++ b/fluxer_app/src/features/voice/state/VoiceP2pConsent.ts @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {makeAutoObservable} from 'mobx'; + +class VoiceP2pConsent { + agreedChannelId: string | null = null; + pendingChannelId: string | null = null; + + constructor() { + makeAutoObservable(this, {}, {autoBind: true}); + } + + agree(channelId: string): void { + this.pendingChannelId = channelId; + } + + commit(channelId: string): void { + this.agreedChannelId = this.isAgreed(channelId) ? channelId : null; + this.pendingChannelId = null; + } + + isAgreed(channelId: string): boolean { + return this.agreedChannelId === channelId || this.pendingChannelId === channelId; + } + + revoke(channelId: string): void { + if (this.agreedChannelId === channelId) this.agreedChannelId = null; + if (this.pendingChannelId === channelId) this.pendingChannelId = null; + } + + clear(): void { + this.agreedChannelId = null; + this.pendingChannelId = null; + } +} + +export default new VoiceP2pConsent(); diff --git a/fluxer_app/src/features/voice/state/VoiceP2pRollout.ts b/fluxer_app/src/features/voice/state/VoiceP2pRollout.ts new file mode 100644 index 000000000..46f0992e5 --- /dev/null +++ b/fluxer_app/src/features/voice/state/VoiceP2pRollout.ts @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import ExperimentAssignments from '@app/features/experiment/state/ExperimentAssignments'; +import SessionManager from '@app/features/platform/state/AuthSession'; +import {Logger} from '@app/features/platform/utils/AppLogger'; +import { + INERT_VOICE_P2P_ASSIGNMENT, + type VoiceP2pAssignmentResponse, +} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; +import { + INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE, + readVoiceP2pAssignment, +} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas'; +import {makeAutoObservable} from 'mobx'; + +const logger = new Logger('VoiceP2pRollout'); + +class VoiceP2pRolloutSelector { + constructor() { + makeAutoObservable(this, {}, {autoBind: true}); + } + + get assignmentReady(): boolean { + return ( + ExperimentAssignments.response !== INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE && + ExperimentAssignments.ownerId === SessionManager.userId + ); + } + + get assignment(): VoiceP2pAssignmentResponse { + if (!this.assignmentReady) { + return INERT_VOICE_P2P_ASSIGNMENT; + } + try { + return readVoiceP2pAssignment(ExperimentAssignments.response); + } catch (err) { + logger.warn('Failed to resolve voice P2P assignment:', err); + return INERT_VOICE_P2P_ASSIGNMENT; + } + } + + get enabled(): boolean { + return this.assignment.enabled; + } + + get maxParticipants(): number { + return this.assignment.max_participants; + } +} + +const VoiceP2pRollout = new VoiceP2pRolloutSelector(); + +export default VoiceP2pRollout; diff --git a/fluxer_app/src/features/voice/state/VoicePrompts.ts b/fluxer_app/src/features/voice/state/VoicePrompts.ts index 79827ca42..779017807 100644 --- a/fluxer_app/src/features/voice/state/VoicePrompts.ts +++ b/fluxer_app/src/features/voice/state/VoicePrompts.ts @@ -8,11 +8,16 @@ import {makeAutoObservable} from 'mobx'; class VoicePrompts { skipHideOwnCameraConfirm = false; skipHideOwnScreenShareConfirm = false; + skipP2pJoinConfirm = false; constructor() { makeAutoObservable( this, - {getSkipHideOwnCameraConfirm: false, getSkipHideOwnScreenShareConfirm: false}, + { + getSkipHideOwnCameraConfirm: false, + getSkipHideOwnScreenShareConfirm: false, + getSkipP2pJoinConfirm: false, + }, {autoBind: true}, ); initializeStore(this, () => this.initPersistence()); @@ -22,14 +27,16 @@ class VoicePrompts { await makeSyncedField(this, { field: 'voicePrompts', schema: VoicePromptsStateSchema, - persist: ['skipHideOwnCameraConfirm', 'skipHideOwnScreenShareConfirm'], + persist: ['skipHideOwnCameraConfirm', 'skipHideOwnScreenShareConfirm', 'skipP2pJoinConfirm'], toMessage: (s) => ({ skipHideOwnCameraConfirm: s.skipHideOwnCameraConfirm, skipHideOwnScreenshareConfirm: s.skipHideOwnScreenShareConfirm, + skipP2pJoinConfirm: s.skipP2pJoinConfirm, }), applyMessage: (s, m) => { s.skipHideOwnCameraConfirm = m.skipHideOwnCameraConfirm; s.skipHideOwnScreenShareConfirm = m.skipHideOwnScreenshareConfirm; + s.skipP2pJoinConfirm = m.skipP2pJoinConfirm; }, }); } @@ -49,6 +56,14 @@ class VoicePrompts { setSkipHideOwnScreenShareConfirm(value: boolean): void { this.skipHideOwnScreenShareConfirm = value; } + + getSkipP2pJoinConfirm(): boolean { + return this.skipP2pJoinConfirm; + } + + setSkipP2pJoinConfirm(value: boolean): void { + this.skipP2pJoinConfirm = value; + } } export default new VoicePrompts(); diff --git a/fluxer_app/src/features/voice/utils/CallUtils.tsx b/fluxer_app/src/features/voice/utils/CallUtils.tsx index a9e6b0ebe..7e2b73a5a 100644 --- a/fluxer_app/src/features/voice/utils/CallUtils.tsx +++ b/fluxer_app/src/features/voice/utils/CallUtils.tsx @@ -2,6 +2,7 @@ import Accessibility from '@app/features/accessibility/state/Accessibility'; import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal'; +import Channels from '@app/features/channel/state/Channels'; import { START_VIDEO_CALL_DESCRIPTOR, START_VOICE_CALL_DESCRIPTOR, @@ -11,6 +12,9 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import {modal} from '@app/features/ui/commands/ModalCommands'; import * as CallCommands from '@app/features/voice/commands/CallCommands'; import {CallNotRingableModal} from '@app/features/voice/components/alerts/CallNotRingableModal'; +import {VoiceP2pConsentModal} from '@app/features/voice/components/alerts/VoiceP2pConsentModal'; +import VoiceP2pConsent from '@app/features/voice/state/VoiceP2pConsent'; +import VoiceP2pRollout from '@app/features/voice/state/VoiceP2pRollout'; import type {I18n} from '@lingui/core'; import {msg} from '@lingui/core/macro'; @@ -66,12 +70,40 @@ async function checkAndStartCall(channelId: string): Promise { } } +function canOfferP2pCall(channelId: string): boolean { + if (!VoiceP2pRollout.enabled) return false; + const channel = Channels.getChannel(channelId); + return channel != null && channel.recipientIds.length + 1 <= VoiceP2pRollout.maxParticipants; +} + export async function requestStartCall( i18n: I18n, channelId: string, options: CallStartConfirmationOptions = {}, ): Promise { const {bypassConfirm = false, kind = 'voice', showShiftBypassConfirmationTip = false} = options; + if (!bypassConfirm && canOfferP2pCall(channelId)) { + ModalCommands.push( + modal(() => ( + { + VoiceP2pConsent.agree(channelId); + if (!(await checkAndStartCall(channelId))) VoiceP2pConsent.revoke(channelId); + }} + onStandard={() => { + VoiceP2pConsent.revoke(channelId); + void checkAndStartCall(channelId); + }} + onCancel={() => {}} + data-flx="voice.call-utils.request-start-call.voice-p2p-consent-modal" + /> + )), + ); + return; + } + VoiceP2pConsent.revoke(channelId); const shouldConfirm = Accessibility.confirmBeforeStartingCalls && !bypassConfirm; if (!shouldConfirm) { await checkAndStartCall(channelId); diff --git a/fluxer_app/src/features/voice/utils/VideoQualityEntitlement.ts b/fluxer_app/src/features/voice/utils/VideoQualityEntitlement.ts index 8ef263489..24c22b4bd 100644 --- a/fluxer_app/src/features/voice/utils/VideoQualityEntitlement.ts +++ b/fluxer_app/src/features/voice/utils/VideoQualityEntitlement.ts @@ -2,15 +2,19 @@ import {LimitResolver} from '@app/features/app/utils/LimitResolverAdapter'; import {isLimitToggleEnabled} from '@app/features/app/utils/LimitUtils'; +import {isActiveVoiceChannelP2p} from '@app/features/voice/state/ChannelP2pStatus'; export function hasHigherVideoQuality(): boolean { - return isLimitToggleEnabled( - { - feature_higher_video_quality: LimitResolver.resolve({ - key: 'feature_higher_video_quality', - fallback: 0, - }), - }, - 'feature_higher_video_quality', + return ( + isActiveVoiceChannelP2p() || + isLimitToggleEnabled( + { + feature_higher_video_quality: LimitResolver.resolve({ + key: 'feature_higher_video_quality', + fallback: 0, + }), + }, + 'feature_higher_video_quality', + ) ); } diff --git a/fluxer_app/src/features/voice/utils/VoiceMessageDescriptors.ts b/fluxer_app/src/features/voice/utils/VoiceMessageDescriptors.ts index 03130a3b3..51bba06b9 100644 --- a/fluxer_app/src/features/voice/utils/VoiceMessageDescriptors.ts +++ b/fluxer_app/src/features/voice/utils/VoiceMessageDescriptors.ts @@ -328,6 +328,132 @@ export const VOICE_CALL_E2EE_BROKEN_DESCRIPTOR = msg({ comment: 'Pre-join indicator shown beneath the Join button on a DM or group DM call where at least one connected participant (typically an outdated bot) does not support E2EE.', }); +export const VOICE_P2P_INDICATOR_DESCRIPTOR = msg({ + message: 'P2P', + comment: + 'Short indicator shown beneath the Join button and in the call footer when the call is peer-to-peer. P2P means peer-to-peer: audio and video go directly between participants instead of through a server.', +}); +export const VOICE_P2P_INDICATOR_TOOLTIP_DESCRIPTOR = msg({ + message: 'Direct connection. Media is encrypted between participants.', + comment: 'Tooltip on the P2P indicator of a peer-to-peer call.', +}); +export const VOICE_P2P_START_TITLE_DESCRIPTOR = msg({ + message: 'Start in P2P mode?', + comment: + 'Title of the dialog offering to start a call in peer-to-peer mode, where audio and video go directly between participants.', +}); +export const VOICE_P2P_JOIN_TITLE_DESCRIPTOR = msg({ + message: 'Join a P2P call?', + comment: + 'Title of the dialog shown before joining a peer-to-peer call, where audio and video go directly between participants.', +}); +export const VOICE_P2P_DIRECT_MEDIA_DESCRIPTOR = msg({ + message: 'Audio and video go directly between participants.', + comment: 'Lead sentence of the P2P call dialog explaining peer-to-peer mode.', +}); +export const VOICE_P2P_LOWER_LATENCY_DESCRIPTOR = msg({ + message: 'Lower latency.', + comment: 'Bullet point in the P2P call dialog. A benefit of peer-to-peer mode.', +}); +export const VOICE_P2P_STREAM_QUALITY_DESCRIPTOR = msg({ + message: 'Everyone in the call gets Plutonium stream quality for free: up to 4K source resolution and 60 fps.', + comment: + 'Bullet point in the P2P call dialog. Plutonium is the name of the paid subscription and must not be translated. 4K and fps are video terms.', +}); +export const VOICE_P2P_IP_ADDRESS_DESCRIPTOR = msg({ + message: 'Other participants can see your IP address.', + comment: 'Bullet point in the P2P call dialog. A privacy drawback of peer-to-peer mode.', +}); +export const VOICE_P2P_MAX_PARTICIPANTS_DESCRIPTOR = msg({ + message: 'Up to {maxParticipants, plural, one {# person} other {# people}}.', + comment: + 'Bullet point in the P2P call dialog. maxParticipants is the participant cap of a peer-to-peer call, a number from 2 to 4.', +}); +export const VOICE_P2P_CONNECT_DIRECTLY_DESCRIPTOR = msg({ + message: 'Anyone who cannot connect directly cannot take part until someone switches the call to a standard call.', + comment: + 'Bullet point in the P2P call dialog. Some networks block direct connections. A standard call goes through a server.', +}); +export const VOICE_P2P_START_CONFIRM_DESCRIPTOR = msg({ + message: 'Start in P2P mode', + comment: 'Primary button in the dialog offering to start a call in peer-to-peer mode.', +}); +export const VOICE_P2P_START_STANDARD_DESCRIPTOR = msg({ + message: 'Start standard call', + comment: + 'Button in the dialog offering to start a call in peer-to-peer mode. Starts an ordinary call that goes through a server instead.', +}); +export const VOICE_P2P_JOIN_CONFIRM_DESCRIPTOR = msg({ + message: 'Join', + comment: 'Primary button in the dialog shown before joining a peer-to-peer call.', + context: 'voice-p2p-consent', +}); +export const VOICE_P2P_ALWAYS_AGREE_DESCRIPTOR = msg({ + message: 'Do not ask again, always agree', + comment: + 'Checkbox in the dialog shown before joining a peer-to-peer call. When checked, future joins skip the dialog.', +}); +export const VOICE_P2P_ALWAYS_AGREE_SETTING_DESCRIPTOR = msg({ + message: 'Join P2P calls without asking', + comment: + 'Switch label in the keyboard accessibility settings. When on, joining a peer-to-peer call skips the confirmation dialog.', +}); +export const VOICE_P2P_ALWAYS_AGREE_SETTING_DESCRIPTION_DESCRIPTOR = msg({ + message: 'Other participants in a P2P call can see your IP address.', + comment: 'Description under the switch that skips the confirmation dialog before joining a peer-to-peer call.', +}); +export const VOICE_P2P_SWITCH_TO_STANDARD_DESCRIPTOR = msg({ + message: 'Switch to standard call', + comment: + 'Action that converts the current peer-to-peer call into an ordinary call that goes through a server. Everyone in the call moves over.', +}); +export const VOICE_P2P_START_CALL_DESCRIPTOR = msg({ + message: 'Start P2P call', + comment: + 'Context-menu action on an empty voice channel that starts a peer-to-peer call there, where audio and video go directly between participants.', +}); +export const VOICE_P2P_PEER_FAILED_TITLE_DESCRIPTOR = msg({ + message: "Couldn't connect to {names}", + comment: + 'Title of the dialog shown when a direct connection to one or more participants of a peer-to-peer call failed. names is a localized list of display names.', +}); +export const VOICE_P2P_PEER_FAILED_DESCRIPTION_DESCRIPTOR = msg({ + message: "A direct connection couldn't be made. You can't hear or see each other.", + comment: 'Body of the dialog shown when a direct connection to a participant of a peer-to-peer call failed.', +}); +export const VOICE_P2P_PEER_FAILED_SWITCH_HINT_DESCRIPTOR = msg({ + message: 'A standard call goes through a server and works for everyone.', + comment: + 'Second sentence of the peer-to-peer connection failure dialog, shown when the call can be switched to an ordinary call.', +}); +export const VOICE_P2P_STAY_DESCRIPTOR = msg({ + message: 'Stay', + comment: + 'Button in the peer-to-peer connection failure dialog that closes the dialog and keeps the user in the call with the participants they can reach.', +}); +export const VOICE_P2P_LEAVE_DESCRIPTOR = msg({ + message: 'Leave call', + comment: 'Button in the peer-to-peer connection failure dialog that leaves the call.', +}); +export const VOICE_P2P_UNAVAILABLE_DESCRIPTOR = msg({ + message: 'P2P calls are unavailable right now.', + comment: 'Error shown when the server refuses to start or join a peer-to-peer call.', +}); +export const VOICE_P2P_CONSENT_REQUIRED_DESCRIPTOR = msg({ + message: "Couldn't join the P2P call. Try again.", + comment: 'Error shown when the server refused to join a peer-to-peer call even though the user agreed to it.', +}); +export const CHANNEL_RTC_P2P_SETTING_DESCRIPTOR = msg({ + message: 'P2P calls only', + comment: + 'Switch label in voice channel settings. When on, every call in this channel is peer-to-peer, where audio and video go directly between participants.', +}); +export const CHANNEL_RTC_P2P_SETTING_DESCRIPTION_DESCRIPTOR = msg({ + message: + "Audio and video go directly between participants, up to {maxParticipants, plural, one {# person} other {# people}}. Everyone agrees before joining and can see each other's IP address.", + comment: + 'Description under the P2P calls only switch in voice channel settings. maxParticipants is the participant cap, a number from 2 to 4.', +}); export const SCREEN_SHARE_STATUS_SOURCE_RESOLUTION_DESCRIPTOR = msg({ message: 'Source', diff --git a/fluxer_docs/astro.config.ts b/fluxer_docs/astro.config.ts index 375d23e53..01d015aa4 100644 --- a/fluxer_docs/astro.config.ts +++ b/fluxer_docs/astro.config.ts @@ -290,7 +290,13 @@ export default defineConfig({ }, { label: 'Voice', - items: ['voice', 'http-api/calls', 'http-api/streams', 'http-api/entrance-sounds'], + items: [ + 'voice', + 'http-api/calls', + 'http-api/peer-to-peer-voice', + 'http-api/streams', + 'http-api/entrance-sounds', + ], }, { label: 'Admin API', diff --git a/fluxer_docs/src/content/docs/admin-api/instance.mdx b/fluxer_docs/src/content/docs/admin-api/instance.mdx index 9b35e78f2..337951427 100644 --- a/fluxer_docs/src/content/docs/admin-api/instance.mdx +++ b/fluxer_docs/src/content/docs/admin-api/instance.mdx @@ -36,6 +36,7 @@ Missing settings use the defaults documented below. Invalid stored configuration | gateway_rollout | [Gateway rollout configuration](#gateway-rollout-configuration-object) object | Gateway admission and dispatch tuning | | push_relay | [push relay configuration](#push-relay-configuration-object) object | Operator consent to the Fluxer-run push relay | | domain_migration | [domain migration configuration](#domain-migration-configuration-object) object | Web domain migration rollout | +| voice_p2p | [peer-to-peer voice configuration](#peer-to-peer-voice-configuration-object) object | Peer-to-peer voice rollout | | captcha | [captcha configuration](#captcha-configuration-object) object | ALTCHA proof-of-work captcha settings | | experiment_delivery | [experiment delivery configuration](#experiment-delivery-configuration-object) object | Cadence every client polls the experiments route on | | registration | [registration configuration](#registration-configuration-object) object | Registration policy, issued URLs, and pending registrations | @@ -139,6 +140,7 @@ The instance rollout that moves the official web client from its legacy origin t | enabled | boolean | Whether the rollout runs at all (default false) | | config_version | integer | Revision counter, raised by Fluxer and never accepted from a request | | rollout_basis_points | integer | Share of accounts the rollout selects, in basis points (0-10000, default 0) | +| rollout_country_codes | array[string] | Countries the sampled share is limited to, as unique uppercase ISO 3166-1 alpha-2 codes, up to 250 (default empty) | | rollout_salt | string | Salt of the sampling hash (1-64 printable ASCII characters, default `domain-migration-v1`) | | included_user_ids | array[snowflake] | Accounts the rollout always selects, up to 1000 entries (default empty) | | included_guild_ids | array[snowflake] | Guilds whose members the rollout always selects, up to 1000 entries (default empty) | @@ -151,6 +153,8 @@ Every field is present on read. An absent document or missing field uses the def `excluded_user_ids` wins over every other rule. Otherwise the rollout selects an account in `included_user_ids`, a member of a guild in `included_guild_ids`, or a premium account while `include_premium_users` is true, whatever `rollout_basis_points` says. +While `rollout_country_codes` is empty, `rollout_basis_points` samples every account. Once it lists a country, the sampled share only selects an account whose request comes from a listed country, and a request with an unknown country is never sampled. The list does not affect `excluded_user_ids`, `included_user_ids`, `included_guild_ids`, `include_premium_users`, or `anonymous_rollout_basis_points`. + An installed Chromium desktop web app moves its data like a browser tab, then stays on the legacy origin and offers to install the app from the new one. Set `standalone_forwarding` once the web app manifest lists the new origin in `scope_extensions` and the new origin serves the matching association file. From then on the installed app forwards like a browser tab. Installed mobile and Safari web apps never forward, whatever the value. Only the official web client acts on this configuration. On any other instance it changes nothing a client does. @@ -159,6 +163,39 @@ Only the official web client acts on this configuration. On any other instance i Setting `enabled` to false stops new migrations and also stops forwarding from the legacy origin for clients that already moved. Excluding an account only stops a migration that has not started yet. ::: +## Peer-to-peer voice configuration object + +The instance rollout that lets small voice calls connect their participants directly instead of through the voice servers. [Experiments](/http-api/experiments/#peer-to-peer-voice-assignment-object) defines what a client resolves from it. + +### Structure + +| Field | Type | Description | +| --- | --- | --- | +| enabled | boolean | Whether the rollout runs at all (default false) | +| config_version | integer | Revision counter, raised by Fluxer and never accepted from a request | +| rollout_basis_points | integer | Share of accounts the rollout selects, in basis points (0-10000, default 0) | +| rollout_country_codes | array[string] | Countries the sampled share is limited to, as unique uppercase ISO 3166-1 alpha-2 codes, up to 250 (default empty) | +| rollout_salt | string | Salt of the sampling hash (1-64 printable ASCII characters, default `voice-p2p-v1`) | +| included_user_ids | array[snowflake] | Accounts the rollout always selects, up to 1000 entries (default empty) | +| included_guild_ids | array[snowflake] | Guilds whose members the rollout always selects, up to 1000 entries (default empty) | +| include_premium_users | boolean | Whether the rollout always selects accounts with active premium (default false) | +| excluded_user_ids | array[snowflake] | Accounts the rollout never selects, up to 1000 entries (default empty) | +| max_participants | integer | Most participants a peer-to-peer call holds (2-4, default 2) | + +Every field is present on read. An absent document or missing field uses the defaults above. + +`excluded_user_ids` wins over every other rule. Otherwise the rollout selects an account in `included_user_ids`, a member of a guild in `included_guild_ids`, or a premium account while `include_premium_users` is true, whatever `rollout_basis_points` says. + +While `rollout_country_codes` is empty, `rollout_basis_points` samples every account. Once it lists a country, the sampled share only selects an account whose request comes from a listed country, and a request with an unknown country is never sampled. The list does not affect `excluded_user_ids`, `included_user_ids`, `included_guild_ids`, or `include_premium_users`. + +The rules above select who may start a peer-to-peer call. Joining a call that already runs peer-to-peer needs only `enabled`, so a partial rollout does not keep unselected accounts out of running calls. + +`max_participants` applies to every account, selected or not. A join that would take a peer-to-peer call past it is refused as full, as [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) describes. Lowering it removes nobody from a running call. + +:::caution[`enabled` is the kill switch] +Setting `enabled` to false makes every new call use the voice servers. A call that already runs peer-to-peer keeps running and refuses new joins until it ends or a participant switches it to the voice servers. Fluxer never moves a running call on its own. +::: + ## Captcha configuration object The ALTCHA proof-of-work check that the API issues and verifies itself on gated operations. [CAPTCHA handling](/topics/captcha/) defines the challenge exchange and lists the gated operations. @@ -611,6 +648,7 @@ The body has one optional object for each section. Fluxer leaves an absent secti | gateway_rollout? | object | Any subset of the [Gateway rollout configuration](#gateway-rollout-configuration-object) fields, each bound as documented there | | push_relay? | object | `relay_consent_accepted` from the [push relay configuration](#push-relay-configuration-object) | | domain_migration? | object | Any subset of the [domain migration](#domain-migration-configuration-object) fields | +| voice_p2p? | object | Any subset of the [peer-to-peer voice](#peer-to-peer-voice-configuration-object) fields | | captcha? | object | Any subset of the [captcha configuration](#captcha-configuration-object) fields | | experiment_delivery? | object | Any subset of the [experiment delivery](#experiment-delivery-configuration-object) fields | | registration? | object | `mode` and `admin_registration_urls_enabled` | @@ -628,6 +666,8 @@ The body has one optional object for each section. Fluxer leaves an absent secti `domain_migration` takes every [domain migration configuration](#domain-migration-configuration-object) field except `config_version`, each bound as documented there. Fluxer raises `config_version` by one on each request that supplies at least one of them. A section that is absent, or present with no field set, writes nothing and leaves `config_version` alone. +`voice_p2p` works the same way, over the [peer-to-peer voice configuration](#peer-to-peer-voice-configuration-object) fields and its own `config_version`. + `captcha` takes any subset of the [captcha configuration](#captcha-configuration-object) fields, each bound as documented there. Fluxer merges the supplied fields over the stored ones. The section has no `config_version`. `experiment_delivery` takes both [experiment delivery configuration](#experiment-delivery-configuration-object) fields, each bound as documented there. It is a section of its own, so a write to it changes no `config_version` and changes no assignment, only the cadence on which clients ask for one. @@ -668,7 +708,7 @@ On a self-hosted deployment, billing and the `everyone` premium mode exclude eac | 400 | [error response](/admin-api/#error-response) | A policy transition is refused, returned as `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED` | :::caution[Sections are applied one after another] -The order is `gateway_rollout`, `push_relay`, `domain_migration`, `captcha`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, `billing`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written. +The order is `gateway_rollout`, `push_relay`, `domain_migration`, `voice_p2p`, `captcha`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, `billing`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written. ::: ### Side effects diff --git a/fluxer_docs/src/content/docs/gateway/commands.md b/fluxer_docs/src/content/docs/gateway/commands.md index c7572e8e8..41a01448f 100644 --- a/fluxer_docs/src/content/docs/gateway/commands.md +++ b/fluxer_docs/src/content/docs/gateway/commands.md @@ -15,8 +15,9 @@ Except for [Heartbeat](#heartbeat), [Identify](#identify), and [Resume](#resume) | 1 | Heartbeat | Opcode `11` Heartbeat ACK | | 2 | Identify | [Ready](/gateway/events/#ready), a close frame, or no response when Fluxer discards a rate-limited Identify or holds it to retry the session start | | 3 | Presence Update | No direct response | -| 4 | Voice State Update | [Voice State Update](/gateway/events/#voice-state-update) and [Voice Server Update](/gateway/events/#voice-server-update) when state changes | +| 4 | Voice State Update | [Voice State Update](/gateway/events/#voice-state-update) and [Voice Server Update](/gateway/events/#voice-server-update) when state changes, or [Gateway Error](/gateway/opcodes-and-close-codes/#gateway-error) for a reported refusal | | 6 | Resume | Replayed Dispatches followed by [Resumed](/gateway/events/#resumed), Invalid Session, or a close frame | +| 13 | Voice Signal | [Voice Signal](/gateway/events/#voice-signal) for the target connection's session, or no response | | 8 | Request Guild Members | One or more [Guild Members Chunk](/gateway/events/#guild-members-chunk) events | | 14 | Lazy Request | [Guild Sync](/gateway/events/#guild-sync), [Guild Member List Update](/gateway/events/#guild-member-list-update), [Thread List Sync](/gateway/threads/#thread-list-sync), and [Thread Member List Update](/gateway/threads/#thread-member-list-update) | | 15 | Request Guild Counts | [Guild Counts Update](/gateway/events/#guild-counts-update) | @@ -267,6 +268,7 @@ Opcode `4` joins, moves, updates, or leaves the voice membership associated with | viewer_stream_keys?3 | ?array[string] | The stream keys this connection is watching, where an omitted key keeps the current list and null clears it | | latitude? | number or string | The client latitude, used to pick a voice region | | longitude? | number or string | The client longitude, used to pick a voice region | +| p2p?5 | boolean | Whether the client agrees to peer-to-peer media for a new connection, or leaves the mesh on an existing one | 1 A `channel_id` with no `connection_id` opens a new connection, and a `channel_id` with one updates or moves that connection. An update that leaves one guild, meaning a non-null `guild_id` with `channel_id: null`, requires a `connection_id`, and one that omits it is refused with `VOICE_MISSING_CONNECTION_ID` @@ -276,6 +278,8 @@ Opcode `4` joins, moves, updates, or leaves the voice membership associated with 4 Only `true` and the string `"true"` set it, and Fluxer publishes `false` when the member lacks `STREAM` in the channel. The screenshare track uses this same connection, so setting the flag issues no grant and sends no [Voice Server Update](/gateway/events/#voice-server-update) +5 Only the JSON Boolean `true` agrees and only the JSON Boolean `false` switches. On a new connection `true` states that the client supports peer-to-peer media and accepts it for this join. A bot session never agrees, whatever it sends. On an existing connection an explicit `false` converts the whole channel or call to LiveKit, and an omitted key or `true` changes nothing. [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) states when a join is peer-to-peer and when it is refused + ```json { "op": 4, @@ -298,10 +302,46 @@ Every field is optional. A non-null `guild_id` or `channel_id` is a canonical de The command has no `session_id` field. Fluxer identifies the voice membership by the Gateway session that sends the command. -Joining or replacing a grant produces [Voice Server Update](/gateway/events/#voice-server-update) with the token and endpoint for the media connection, and [Voice State Update](/gateway/events/#voice-state-update) for every session that can see the channel. +Joining or replacing a grant produces [Voice Server Update](/gateway/events/#voice-server-update) with the token and endpoint for the media connection, or with the ICE servers of a peer-to-peer grant, and [Voice State Update](/gateway/events/#voice-state-update) for every session that can see the channel. + +A refusal with `VOICE_P2P_CONSENT_REQUIRED`, `VOICE_P2P_UNAVAILABLE`, or `VOICE_CHANNEL_FULL` reaches the requesting session as a [Gateway Error](/gateway/opcodes-and-close-codes/#gateway-error) payload. Every other refusal sends nothing. The first two updates in a rolling one-second window take effect immediately. Later updates can be delayed or replaced by newer updates for the same `guild_id` and `connection_id`. See [command rate limits](/gateway/limits-and-rate-limits/#connection-and-command-rate-limits) for the timing and capacity bounds. +## Voice Signal + +Opcode `13` sends one signalling message to one other connection of a [peer-to-peer](/gateway/events/#peer-to-peer-voice) voice channel or call. + +| Field | Type | Description | +| --- | --- | --- | +| guild_id? | ?snowflake | The guild containing the voice channel, where null or an omitted key selects the DM and group DM call context | +| channel_id | snowflake | The voice channel or call channel both connections are in | +| to | string | The `connection_id` of the voice state the signal is for | +| data | object | The signalling message, which Fluxer relays unread | + +```json +{ + "op": 13, + "d": { + "guild_id": "1189375284394692608", + "channel_id": "1189375284394692610", + "to": "b7f3c2a94e1d4c0f", + "data": { + "type": "offer", + "sdp": "v=0..." + } + } +} +``` + +Fluxer relays the signal when the sending session owns a voice state with `p2p: true` in that channel and `to` names another voice state with `p2p: true` in the same channel. The target connection's session alone receives [Voice Signal](/gateway/events/#voice-signal). + +Every other signal is dropped with no event and no close. That covers a channel or call that is not peer-to-peer, a `to` that names no connection there, a `to` that names the sender's own connection, a `data` that is not an object, and a session with no voice state in the channel. + +`data` is opaque to Fluxer. Clients agree on its contents among themselves, typically a session description or an ICE candidate. The whole frame stays within the [inbound payload bound](/gateway/overview/#framing). + +A client sends this command only after it has received a [Voice Server Update](/gateway/events/#voice-server-update) with `p2p: true` for the connection. [Command rate limits](/gateway/limits-and-rate-limits/#connection-and-command-rate-limits) states the Voice Signal budget. + ## Request Guild Members Opcode `8` requests bounded member chunks. diff --git a/fluxer_docs/src/content/docs/gateway/events.md b/fluxer_docs/src/content/docs/gateway/events.md index 9c8162eff..ed83623ba 100644 --- a/fluxer_docs/src/content/docs/gateway/events.md +++ b/fluxer_docs/src/content/docs/gateway/events.md @@ -114,6 +114,7 @@ A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it | [Channel Pins ACK](#channel-pins-ack) | The current user acknowledges a channel's pins | Current user | | [Voice State Update](#voice-state-update) | A guild or call participant's voice state changes | Channel visibility | | [Voice Server Update](#voice-server-update) | The session receives or replaces its own voice grant | Current session | +| [Voice Signal](#voice-signal) | Another peer-to-peer voice connection sends this connection a signalling message | Current session | | [Entrance Sound Play](#entrance-sound-play) | A participant's entrance sound plays in a voice channel | Voice channel | | [Call Create](#call-create) | A private channel call begins or becomes visible | Call recipient | | [Call Update](#call-update) | The ringing set, participant roster, or region of a call changes | Call recipient | @@ -985,6 +986,7 @@ A `channel_id` of null means the participant left. | suppress | boolean | Whether the participant is suppressed | | viewer_stream_keys | array[string] | Streams this connection is watching | | e2ee_capable | boolean | Whether the participant's client supports end-to-end encrypted voice | +| p2p | boolean | Whether this connection exchanges media peer-to-peer and not through LiveKit | | version | integer | Monotonic version of this participant's voice state | 1 The participant's client reports this value. In a guild voice channel Fluxer sets it to false when the participant lacks `STREAM` @@ -995,23 +997,122 @@ The broadcast form has no `region_id`, `server_id`, `latitude`, or `longitude`. The session received or replaced its own voice grant. Delivered to the requesting session alone. +A grant has one of two structures. A LiveKit grant has `token` and `endpoint`. A peer-to-peer grant has `p2p: true` and `ice_servers`, and never has `token`, `endpoint`, or `e2ee_key`. + | Field | Type | Description | | --- | --- | --- | -| token | string | The LiveKit access token this connection presents | -| endpoint | string | The LiveKit signalling URL to connect to, a `ws://` or `wss://` address | +| token?1 | string | The LiveKit access token this connection presents | +| endpoint?1 | string | The LiveKit signalling URL to connect to, a `ws://` or `wss://` address | +| p2p?2 | boolean | Always true when present. The connection exchanges media peer-to-peer | +| ice_servers?2 | array[[ICE server object](#ice-server-object)] | The STUN servers the connection uses for its peer connections, empty when the instance names none | | connection_id | string | The voice connection the grant covers | | channel_id | snowflake | The channel the grant covers | -| guild_id?1 | snowflake | The guild the channel belongs to | -| e2ee_key?2 | string | The key material the channel's end-to-end encryption uses | +| guild_id?3 | snowflake | The guild the channel belongs to | +| e2ee_key?4 | string | The key material the channel's end-to-end encryption uses | -1 Present for a guild voice channel and absent for a call, so a client reads the scope from this field +1 Present on a LiveKit grant -2 Present only when the channel is end-to-end encrypted +2 Present on a peer-to-peer grant, which only a session that sent `p2p: true` in its [Voice State Update](/gateway/commands/#voice-state-update) command receives -Fluxer uses LiveKit for voice media. There is no second voice websocket, no voice opcode set, and no UDP discovery step. A client opens a LiveKit connection to `endpoint`, presents `token` there, and speaks the LiveKit protocol from that point on. [Voice](/voice/) states the room naming, the participant identity, and the track sources a grant admits. +3 Present for a guild voice channel and absent for a call, so a client reads the scope from this field + +4 Present only on a LiveKit grant for an end-to-end encrypted channel + +#### ICE server object + +| Field | Type | Description | +| --- | --- | --- | +| urls | array[string] | The `stun:` URLs of one server | + +Fluxer never hands out a TURN server, so a peer-to-peer connection never relays media. Peers that cannot reach each other directly fail to connect, and [Peer-to-peer voice](#peer-to-peer-voice) states what a client does then. + +```json +{ + "p2p": true, + "ice_servers": [ + {"urls": ["stun:stun.example.com:3478"]} + ], + "connection_id": "b7f3c2a94e1d4c0f", + "channel_id": "1189375284394692610", + "guild_id": "1189375284394692608" +} +``` + +Fluxer uses LiveKit for voice media unless the grant is peer-to-peer. There is no second voice websocket and no UDP discovery step. With a LiveKit grant a client opens a LiveKit connection to `endpoint`, presents `token` there, and speaks the LiveKit protocol from that point on. With a peer-to-peer grant a client follows [Peer-to-peer voice](#peer-to-peer-voice). [Voice](/voice/) states the room naming, the participant identity, and the track sources a grant admits. A grant is issued when a connection opens, when it moves to another channel, and when its region changes. Toggling `self_mute`, `self_video`, or `self_stream` produces no new grant. A call region change reissues one grant to each participant of the call. +A LiveKit grant for a `connection_id` that holds a peer-to-peer grant means a participant or a region change converted the channel or call. The client closes its peer connections and connects to LiveKit with the new grant. + +#### Peer-to-peer voice + +A voice channel or call is peer-to-peer when it has at least one voice state and every voice state in it has `p2p: true`. The participants then form a full mesh of WebRTC peer connections, one for each pair of connections, and no media passes through LiveKit. The mesh roster is the set of voice states of the channel or call. + +Fluxer decides each new connection as follows. + +| Channel or call | The command sent `p2p: true` | Any other `p2p` value | +| --- | --- | --- | +| Peer-to-peer below the participant cap | Peer-to-peer join1 | Refused with `VOICE_P2P_CONSENT_REQUIRED` | +| Peer-to-peer at the participant cap | Refused with `VOICE_CHANNEL_FULL` | Refused with `VOICE_P2P_CONSENT_REQUIRED` | +| Empty, with no join awaiting LiveKit confirmation | Peer-to-peer join, or a LiveKit join when Fluxer declines2 | LiveKit join | +| Every other state | LiveKit join | LiveKit join | + +1 Refused with `VOICE_P2P_UNAVAILABLE` when Fluxer declines peer-to-peer for this join + +2 The session then receives a LiveKit grant and the channel or call is not peer-to-peer + +Participants are counted as connections in a guild voice channel and as users in a call. The participant cap is `max_participants` of the [peer-to-peer voice configuration](/admin-api/instance/#peer-to-peer-voice-configuration-object), 2 through 4, and every account reads it from its [peer-to-peer voice assignment](/http-api/experiments/#peer-to-peer-voice-assignment-object). A lower cap never removes a participant from a running call. A bot session counts as not having sent `p2p: true`. A connection that moves into a guild voice channel with its own Voice State Update command is decided by the same table. + +Each refusal reaches the requesting session as a [Gateway Error](/gateway/opcodes-and-close-codes/#gateway-error) payload, changes no voice state, and leaves the existing participants untouched. A client that receives `VOICE_P2P_CONSENT_REQUIRED` asks its user and, on agreement, sends the join again with `p2p: true`. + +Two joins can reach an empty call at the same moment with different `p2p` values. Fluxer admits the first and refuses the other with `VOICE_P2P_CONSENT_REQUIRED`. The refused session receives a [Voice State Update](#voice-state-update) with a null `channel_id` for the connection it had been granted. + +The voice state of a peer-to-peer join is published at once, with no LiveKit confirmation step. Peers exchange session descriptions and ICE candidates with the [Voice Signal](/gateway/commands/#voice-signal) command, which arrives as [Voice Signal](#voice-signal). + +Fluxer never converts a peer-to-peer channel or call to LiveKit by itself. Only two actions convert one. + +- A participant sends a Voice State Update command for its own connection with an explicit `p2p: false`. +- The voice region changes. [Modify call region](/http-api/calls/#modify-call-region) converts a call whenever it supplies `region`, even with the current value. [Modify channel](/http-api/channels/#modify-channel) converts a guild voice channel when it stores a different `rtc_region`. + +A conversion sets `p2p` to false on every voice state of the channel or call, publishes each as a [Voice State Update](#voice-state-update), and sends each connected session a LiveKit grant for its existing `connection_id`. The conversion is one way. The channel or call can be peer-to-peer again only after it has emptied. + +A guild voice channel whose `rtc_p2p` is true stays peer-to-peer. Neither action converts it, and a change to `rtc_p2p` takes effect for the next call, after the channel has emptied. Fluxer does not ask for agreement before the first join of such a channel. A client whose [peer-to-peer voice assignment](/http-api/experiments/#peer-to-peer-voice-assignment-object) is enabled asks its user and sends `p2p: true` on agreement. Any other first join starts a LiveKit call. + +A failed peer connection converts nothing. The affected client reports the problem to its user, who can leave or send the explicit `p2p: false`. Each client reports how its peer connections settled with [Report connection outcomes](/http-api/peer-to-peer-voice/#report-connection-outcomes). + +A moderator cannot move a member into a peer-to-peer channel. [Modify guild member](/http-api/guild-members/#modify-guild-member) returns 400 `USER_NOT_IN_VOICE` and the member stays in its channel. A member moved out of a peer-to-peer channel receives a LiveKit grant for the target channel. + +Server mute and server deafen remain voice state flags in a peer-to-peer mesh, and each peer applies them to the media it sends and plays. A peer closes its peer connection to a connection whose voice state leaves the channel. + +### VOICE_SIGNAL + +Another connection of a [peer-to-peer](#peer-to-peer-voice) voice channel or call sent this session a signalling message with the [Voice Signal](/gateway/commands/#voice-signal) command. Delivered to the session that owns the target connection alone. + +| Field | Type | Description | +| --- | --- | --- | +| guild_id?1 | snowflake | The guild the channel belongs to | +| channel_id | snowflake | The voice channel or call channel | +| from | string | The `connection_id` of the sending voice state | +| user_id | snowflake | The user who owns the sending connection | +| data | object | The signalling message exactly as the sender supplied it | + +1 Present for a guild voice channel and absent for a call + +```json +{ + "guild_id": "1189375284394692608", + "channel_id": "1189375284394692610", + "from": "9a41d07c5be2f318", + "user_id": "1189375284394692700", + "data": { + "type": "answer", + "sdp": "v=0..." + } +} +``` + +Fluxer authorises every signal against the current voice states, so `from` always names a connection with `p2p: true` in `channel_id`. A client discards a signal whose `from` it no longer has in its roster. + ### ENTRANCE_SOUND_PLAY A participant asked for their entrance sound to play in a voice channel they are already connected to. diff --git a/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md b/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md index 285e323a0..23c2af0fd 100644 --- a/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md +++ b/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md @@ -8,7 +8,7 @@ Every main Gateway connection has limits on what it sends, how long its session ## Transport and encoding -[Framing](/gateway/overview/#framing) owns the protocol version, the payload bound, and the compression contract. One inbound WebSocket message is limited to 4,096 bytes on the wire and to a further 4,096 bytes after decompression, and either bound closes with `4002` and reason `Payload too large`. +[Framing](/gateway/overview/#framing) owns the protocol version, the payload bound, and the compression contract. One inbound WebSocket message is limited to 16,384 bytes on the wire and to a further 16,384 bytes after decompression, and either bound closes with `4002` and reason `Payload too large`. A compressed message that decompresses past 10 MiB closes with `4002` and reason `Decompression failed`. That message never closes with `Payload too large`. @@ -50,6 +50,8 @@ Presence Update accepts five commands per WebSocket in a rolling 20-second windo Voice State Update processes the first two commands per session in a rolling one-second window immediately. Later updates enter a per-session queue that holds at most 64 commands and drains one command every 500 ms. A newer update replaces an older queued update for the same `guild_id` and `connection_id` pair, and a full queue discards its oldest entry before accepting the new one. +Voice Signal accepts 240 commands per WebSocket in a rolling 10-second window, which leaves room for a burst of trickle ICE candidates. A further signal is discarded without closing the connection. Every signal, discarded or not, also counts toward the three payload budgets above. + Request Guild Members has three command-specific budgets. Each account may send 12 requests in a rolling 10-second window, and each guild accepts 40 requests in a rolling 10-second window across all accounts. A request over either of these budgets is discarded without a result and without a close. A bot requesting a complete member list is limited to one accepted request per guild every 30 seconds, and a request inside that window produces [Rate Limited](/gateway/events/#rate-limited). The budget is keyed by the account and the guild together, so reconnecting does not reset it. ## Bounded commands @@ -94,8 +96,10 @@ Identify accepts at most 256 `ignored_events` entries. A longer array closes wit ## Voice admission -Voice admission follows the enclosing guild, DM, group DM, channel, and permission rules. A refusal produces no event and closes nothing. +Voice admission follows the enclosing guild, DM, group DM, channel, and permission rules. A refusal closes nothing. It produces a [Gateway Error](/gateway/opcodes-and-close-codes/#gateway-error) for `VOICE_CHANNEL_FULL`, `VOICE_P2P_CONSENT_REQUIRED`, and `VOICE_P2P_UNAVAILABLE`, and no payload for every other code. A guild voice channel admits at most `user_limit` users, where `0` means unlimited. A channel in which any participant has a camera enabled also admits at most 25 users in total, whatever its `user_limit`. A channel that already holds 25 users with cameras enabled refuses a further camera with `VOICE_CAMERA_USER_LIMIT`. [Capacity](/voice/#capacity) states these bounds in full. +A peer-to-peer mesh holds at most 4 participants, counted as connections in a guild voice channel and as users in a call. A join past that bound is refused with `VOICE_CHANNEL_FULL`, as [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) describes. + One user holds at most `voice_connection_limit` simultaneous voice connections in one guild voice channel. The field is part of the [channel object](/http-api/channels/#channel-object), defaults to 5, and is accepted from 1 through 100. Pending connections that have not yet expired count against it. diff --git a/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md b/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md index d54c4bada..0cd7d5d94 100644 --- a/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md +++ b/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md @@ -22,7 +22,8 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa | 9 | Invalid Session1 | Server to client | | 10 | Hello | Server to client | | 11 | Heartbeat ACK | Server to client | -| 12 | Gateway Error | Reserved | +| 12 | Gateway Error | Server to client | +| 13 | Voice Signal | Client to server | | 14 | Lazy Request | Client to server | | 15 | Request Guild Counts | Client to server | | 16 | Request Channel Member Counts | Client to server | @@ -46,7 +47,8 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa | 9 | Literal `false` | The session named by Resume could not be reached, the supplied `seq` is below the replay floor, or an established session ended6 | | 10 | Object with `heartbeat_interval` | Sent as the WebSocket is accepted, before any client payload is read | | 11 | No `d` field | Acknowledges an accepted client heartbeat | -| 12 | Not implemented | Never sent. Errors reach the client as a Dispatch or a close frame | +| 12 | [Gateway Error object](#gateway-error) | Fluxer reports a refused voice join that the client is expected to act on | +| 13 | [Voice Signal object](/gateway/commands/#voice-signal) | The client sends peer-to-peer signalling to one other voice connection | | 14 | [Lazy Request object](/gateway/commands/#lazy-request) | The client replaces bounded guild subscriptions | | 15 | [Request Guild Counts object](/gateway/commands/#request-guild-counts) | The client requests current guild count records | | 16 | [Request Channel Member Counts object](/gateway/commands/#request-channel-member-counts) | The client requests channel count records | @@ -56,7 +58,7 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa 2 Answer a server heartbeat request immediately, in addition to the regular schedule -3 A client that sends Opcode 5 or 12 gets the same close as a client that sent an undefined opcode +3 A client that sends Opcode 5 or 12 gets the same close as a client that sent an undefined opcode. Fluxer never sends Opcode 5 4 Resume is accepted whether or not a session is already attached to the connection @@ -64,7 +66,7 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa 6 After the frame, a socket whose session ended is unauthenticated. After a failed Resume, a socket that already held a session still holds it -The registry is complete. Opcode 13, the values 17 through 27, and every value above 28 are undefined. +The registry is complete. The values 17 through 27 and every value above 28 are undefined. Fluxer resolves an inbound payload in this order. @@ -72,7 +74,7 @@ Fluxer resolves an inbound payload in this order. 2. Heartbeat and Resume are handled whether or not a session is attached. 3. Identify closes with `4005` when a session is already attached. 4. Every remaining opcode closes with `4003` while no session is attached. -5. With a session attached, Presence Update, Voice State Update, Request Guild Members, Lazy Request, Request Guild Counts, Request Channel Member Counts, and Request Forum Unreads are handled. Every other opcode, including a server opcode and an undefined value, closes with `4001`. +5. With a session attached, Presence Update, Voice State Update, Voice Signal, Request Guild Members, Lazy Request, Request Guild Counts, Request Channel Member Counts, and Request Forum Unreads are handled. Every other opcode, including a server opcode and an undefined value, closes with `4001`. Request Forum Unreads from a user session that did not set [`CHANNEL_THREADS`](/gateway/threads/#session-flag) also closes with `4001`. @@ -80,6 +82,33 @@ Request Forum Unreads from a user session that did not set [`CHANNEL_THREADS`](/ A client SHOULD log an unknown opcode and ignore the frame, and MUST NOT close or reconnect solely because the server used an opcode newer than this registry. ::: +## Gateway Error + +Opcode `12` reports a refused [Voice State Update](/gateway/commands/#voice-state-update) command to the session that sent it. The payload has no `s` and no `t`, is not replayed by [Resume](/gateway/commands/#resume), and closes nothing. + +| Field | Type | Description | +| --- | --- | --- | +| code | string | The refusal code | +| message | string | A description for diagnosis, which a client does not show or parse | + +```json +{ + "op": 12, + "d": { + "code": "VOICE_P2P_CONSENT_REQUIRED", + "message": "This call is peer-to-peer and requires agreement to join" + } +} +``` + +Fluxer sends three codes. + +- `VOICE_P2P_CONSENT_REQUIRED` means the channel or call is peer-to-peer and the join did not send `p2p: true`. Ask the user, then send the join again with `p2p: true`. +- `VOICE_P2P_UNAVAILABLE` means Fluxer declined peer-to-peer for a join into a peer-to-peer channel or call. Abort the join and tell the user. +- `VOICE_CHANNEL_FULL` means the channel is at its `user_limit`, or the peer-to-peer mesh is at its participant cap. Abort the join and tell the user. + +Every other voice refusal sends no payload. [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) states when each of the first two codes is produced. + ## Close codes | Code | Name | Meaning | @@ -185,7 +214,7 @@ The Gateway sends an exact reason string with every application close. Reason strings are stable wire values. A client branches on the code and MAY record the reason for diagnosis. :::note[Some refusals send nothing] -A held or discarded Identify, an over-budget Presence Update, an over-budget [Request Forum Unreads](/gateway/threads/#request-forum-unreads), a dropped bounded request, and a rejected voice state update produce no close and no reason. [Client commands](/gateway/commands/) states which command behaves that way. +A held or discarded Identify, an over-budget Presence Update, an over-budget [Request Forum Unreads](/gateway/threads/#request-forum-unreads), a dropped bounded request, an unroutable or over-budget Voice Signal, and a rejected voice state update produce no close and no reason. A rejected voice state update sends a [Gateway Error](#gateway-error) for three codes and nothing for the rest. [Client commands](/gateway/commands/) states which command behaves that way. ::: ## Ordinary WebSocket closes diff --git a/fluxer_docs/src/content/docs/gateway/overview.md b/fluxer_docs/src/content/docs/gateway/overview.md index 908116301..29a6cbaed 100644 --- a/fluxer_docs/src/content/docs/gateway/overview.md +++ b/fluxer_docs/src/content/docs/gateway/overview.md @@ -90,7 +90,7 @@ A replayed Dispatch keeps its original sequence, and a replayed run can have gap ## Framing -One inbound WebSocket message is limited to 4,096 bytes on the wire, and a compressed inbound message is limited to a further 4,096 bytes after decompression. A message past either bound closes with `4002` and reason `Payload too large`. An inbound message that cannot be decompressed closes with `4002` and reason `Decompression failed`. +One inbound WebSocket message is limited to 16,384 bytes on the wire, and a compressed inbound message is limited to a further 16,384 bytes after decompression. A message past either bound closes with `4002` and reason `Payload too large`. An inbound message that cannot be decompressed closes with `4002` and reason `Decompression failed`. JSON payloads are UTF-8 objects. An uncompressed client payload is sent in a text frame, and a payload the client compressed with the negotiated zstd stream is sent in a binary frame. diff --git a/fluxer_docs/src/content/docs/http-api/calls.mdx b/fluxer_docs/src/content/docs/http-api/calls.mdx index 8f8540755..c27fb6637 100644 --- a/fluxer_docs/src/content/docs/http-api/calls.mdx +++ b/fluxer_docs/src/content/docs/http-api/calls.mdx @@ -100,6 +100,8 @@ Changes the RTC region of an active direct message or group direct message call. The caller does not have to be connected to the call. +Supplying `region` on a [peer-to-peer](/gateway/events/#peer-to-peer-voice) call moves the call to a voice server, even when the value names the region the call already has. The move is one way for the life of that call. + ### Path parameters | Field | Type | Description | diff --git a/fluxer_docs/src/content/docs/http-api/channels.mdx b/fluxer_docs/src/content/docs/http-api/channels.mdx index 3935007f5..e82791e5f 100644 --- a/fluxer_docs/src/content/docs/http-api/channels.mdx +++ b/fluxer_docs/src/content/docs/http-api/channels.mdx @@ -32,6 +32,7 @@ A channel object always has its identity and type. Every other field is present | user_limit?4 | ?integer | The configured member occupancy limit, present only for a guild voice channel | | voice_connection_limit?5 | ?integer | The number of simultaneous voice connections one user may hold, present only for a guild voice channel | | rtc_region?6 | ?string | The ID of the selected RTC region, present only for a guild voice channel | +| rtc_p2p? | boolean | Whether the channel is pinned to [peer-to-peer voice](/gateway/events/#peer-to-peer-voice), present only for a guild voice channel | | last_message_id? | ?snowflake | The ID of the most recent message, null when the channel has none | | last_pin_timestamp? | ?ISO8601 timestamp | The time a message was most recently pinned, or null when nothing has ever been pinned | | permission_overwrites?7 | array[[permission overwrite](#permission-overwrite-object) object] | The overwrites applied to this channel, present only for a guild channel other than a thread | @@ -464,6 +465,7 @@ Every field is optional, and an omitted field preserves its current value. Every | content_warning_text?10 | ?string | The content warning text (max 200 characters) | | rate_limit_per_user?11 | ?integer | The slowmode interval in seconds (0-21,600) | | rtc_region?12 | ?string | The ID of the RTC region (1-64 characters) for a guild voice channel, where null selects automatic routing | +| rtc_p2p? | boolean | Whether to pin a guild voice channel to [peer-to-peer voice](/gateway/events/#peer-to-peer-voice). Ignored on every other channel type | 1 An explicit null preserves the current name. In a guild without flexible channel names, Fluxer lowercases a guild text, announcement, forum, or media channel name, replaces its whitespace with hyphens, and removes disallowed punctuation @@ -572,7 +574,7 @@ A group marked for mature content accepts media the scanner flags as sensitive, A guild channel change emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to its guild. For a forum or media channel the event reaches only a session with [thread access](/gateway/threads/#session-flag). Replacing a category's permission overwrites also replaces them on each child whose overwrites still exactly match the category's previous values, with one Dispatch per changed child. A child whose overwrites had differed is left unchanged. -Changing `rate_limit_per_user` clears the remaining slowmode delay of every user in the channel. Each user's next message is then checked against the new interval. Changing `rtc_region` on a guild voice channel moves its active voice connections to the selected region. +Changing `rate_limit_per_user` clears the remaining slowmode delay of every user in the channel. Each user's next message is then checked against the new interval. Changing `rtc_region` on a guild voice channel moves its active voice connections to the selected region. A [peer-to-peer](/gateway/events/#peer-to-peer-voice) call in the channel moves to a voice server with that change, unless the channel stores `rtc_p2p` true. Changing `rtc_p2p` leaves a running call as it is and applies to the next call that starts after the channel empties. Changing it to true fails with the field code `GUILD_FEATURE_NOT_TOGGLEABLE` unless the caller's [peer-to-peer voice assignment](/http-api/experiments/#peer-to-peer-voice-assignment-object) is enabled. Converting an announcement channel into a text channel deletes every [channel follower webhook](/http-api/webhooks/#webhook-types) that follows it. The deletion runs in the background after the response, emits [Webhooks Update](/gateway/events/#webhooks-update) for each target channel, and writes no audit log entry. Copies already delivered stay in the target channels, and later edits and deletions of the published messages still reach them. diff --git a/fluxer_docs/src/content/docs/http-api/experiments.mdx b/fluxer_docs/src/content/docs/http-api/experiments.mdx index 0c9cb6024..cc30f0cf7 100644 --- a/fluxer_docs/src/content/docs/http-api/experiments.mdx +++ b/fluxer_docs/src/content/docs/http-api/experiments.mdx @@ -6,7 +6,7 @@ description: The experiment assignments envelope, the revalidation and polling c import RouteHeader from '@/components/RouteHeader.astro'; -An experiment is an instance-wide rollout. For each account, Fluxer works out whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `domain_migration`, `plutonium_page`, and `channel_threads`. Only `domain_migration` is configured by an operator. `plutonium_page` and `channel_threads` are on for every account. +An experiment is an instance-wide rollout. For each account, Fluxer works out whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `domain_migration`, `plutonium_page`, `channel_threads`, and `voice_p2p`. Only `domain_migration` and `voice_p2p` are configured by an operator. `plutonium_page` and `channel_threads` are on for every account. Every assignment is advice. A client that ignores one behaves as it does with the rollout off, and no route and no Gateway event reports what a client actually ran. @@ -35,6 +35,7 @@ One entry per experiment. The envelope reports this object even when it is empty | domain_migration? | [domain migration assignment](#domain-migration-assignment-object) object | The caller's web domain migration assignment | | plutonium_page? | [Plutonium page assignment](#plutonium-page-assignment-object) object | The caller's Plutonium page assignment | | channel_threads? | [channel threads assignment](#channel-threads-assignment-object) object | The caller's channel threads assignment | +| voice_p2p? | [peer-to-peer voice assignment](#peer-to-peer-voice-assignment-object) object | The caller's peer-to-peer voice assignment | Ignore unknown experiments and treat a missing experiment as off. @@ -48,7 +49,7 @@ One resolution of the instance web domain migration rollout against one account. | --- | --- | --- | | enabled | boolean | Whether the caller's web client moves to the new web origin | -A caller is drawn by the operator's account or guild allowlist, by having premium when the operator includes premium users, or by the sampled share of the account population. `enabled` is false in every other case, the rollout being off included. +A caller is drawn by the operator's account or guild allowlist, by having premium when the operator includes premium users, or by the sampled share of the account population. When the operator lists rollout countries, the sampled share only draws a caller whose request comes from a listed country. `enabled` is false in every other case, the rollout being off included. Only the official web client acts on this assignment, and only on its legacy origins. Every other client ignores it. The logged-out share and the instance-wide switch are published in the [instance discovery document](/http-api/instance/#domain-migration-object) instead, because a client that holds no credential cannot read this route. @@ -79,6 +80,25 @@ The channel threads state for one account. This server version writes the key on The key stays for clients that still read it. Threads, forum channels, and media channels need the [`channel_threads` client capability](/http-api/threads/#client-capability). +## Peer-to-peer voice assignment object + +One resolution of the instance peer-to-peer voice rollout against one account. This server version writes the key on every response. + +### Structure + +| Field | Type | Description | +| --- | --- | --- | +| enabled | boolean | Whether the caller may start a peer-to-peer voice call | +| max_participants | integer | Most participants a peer-to-peer call holds (2-4) | + +A caller is drawn by the operator's account or guild allowlist, by having premium when the operator includes premium users, or by the sampled share of the account population. When the operator lists rollout countries, the sampled share only draws a caller whose request comes from a listed country. `enabled` is false in every other case, the rollout being off included. + +A selected client may ask for a peer-to-peer connection when it starts a voice call, and shows the peer-to-peer setting of a guild voice channel. It offers a peer-to-peer call in a direct message or group direct message only when the channel has at most `max_participants` members, the caller included. `max_participants` is the same for every account, selected or not, and a join past it is [refused as full](/gateway/events/#peer-to-peer-voice). The assignment does not decide who joins. While the rollout is on, any account that agrees to a direct connection may join a peer-to-peer call that is already running. The server checks the rollout again when a call starts, so a client that ignores this assignment gains nothing. + +## Request country + +Fluxer resolves the country of each request to this route from the client address. An account can receive a different assignment from another network when an operator lists rollout countries. A request whose country is unknown matches no listed country. + ## Get experiment assignments @@ -112,7 +132,7 @@ A client sends the `ETag` it last received. Fluxer compares it against the tag o | Cache-Control | string | The literal value `private, no-cache` | | Vary | string | The literal value `Authorization`, replaced by `Origin` where the [cross-origin policy](/http-api/#cross-origin-requests) echoed an allowed origin | -The `ETag` changes when the response changes. Treat it as an opaque value. +The `ETag` changes when the response changes, which includes a change of [request country](#request-country). Treat it as an opaque value. `ETag` is listed in `Access-Control-Expose-Headers` and `If-None-Match` in `Access-Control-Allow-Headers`, so a cross-origin client reads the tag and revalidates with it. diff --git a/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx b/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx index b25290a5c..30aa63a4b 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx @@ -311,6 +311,7 @@ Recorded by [CHANNEL_CREATE](#audit-actions), [CHANNEL_UPDATE](#audit-actions), | voice_connection_limit | ?number | The configured per-user voice connection limit | | bitrate | ?number | The voice bitrate in bits per second | | rtc_region | ?string | The pinned voice region, or null when the channel is routed automatically | +| rtc_p2p | boolean | Whether the channel is pinned to peer-to-peer voice | | permission_overwrite_count2 | number | The number of permission overwrites the channel has | | flags3 | number | The [channel flags](/http-api/forums/#channel-flags) of the channel | | default_auto_archive_duration3 | number | The stored default [auto archive duration](/http-api/threads/#auto-archive-durations) | diff --git a/fluxer_docs/src/content/docs/http-api/guild-members.mdx b/fluxer_docs/src/content/docs/http-api/guild-members.mdx index 465305d6f..2bab11f5b 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-members.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-members.mdx @@ -308,7 +308,7 @@ Hierarchy authority over the target is not checked for `roles`, so a caller who Fluxer never checks the target's own permissions in the destination, so a member can be moved into a channel they could not join themselves. Disconnecting sets `channel_id` to null and requires the same permission. -Supplying `channel_id` for a target with no live voice connection, or a `connection_id` that is not one of the target's own, returns 400 `USER_NOT_IN_VOICE`. Supplying `mute` or `deaf` for such a target succeeds and stores the flags. A destination that does not exist returns the field code `CHANNEL_DOES_NOT_EXIST`, and one that is not a voice channel `CHANNEL_MUST_BE_VOICE`. +Supplying `channel_id` for a target with no live voice connection, or a `connection_id` that is not one of the target's own, returns 400 `USER_NOT_IN_VOICE`. Supplying `mute` or `deaf` for such a target succeeds and stores the flags. A destination that does not exist returns the field code `CHANNEL_DOES_NOT_EXIST`, and one that is not a voice channel `CHANNEL_MUST_BE_VOICE`. A destination that is [peer-to-peer](/gateway/events/#peer-to-peer-voice) also returns 400 `USER_NOT_IN_VOICE`. `MANAGE_ROLES` and `MODERATE_MEMBERS` are [elevated permissions](/http-api/permissions/#elevated-permissions). When the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, a caller who is not the guild owner also needs an enrolled multi-factor authenticator to replace `roles` or apply `communication_disabled_until`. diff --git a/fluxer_docs/src/content/docs/http-api/peer-to-peer-voice.mdx b/fluxer_docs/src/content/docs/http-api/peer-to-peer-voice.mdx new file mode 100644 index 000000000..583f54f65 --- /dev/null +++ b/fluxer_docs/src/content/docs/http-api/peer-to-peer-voice.mdx @@ -0,0 +1,89 @@ +--- +# SPDX-License-Identifier: AGPL-3.0-or-later +title: Peer-to-peer voice +description: Connection outcome reports for the peer connections of a peer-to-peer voice call. +--- + +import RouteHeader from '@/components/RouteHeader.astro'; + +A peer-to-peer voice call connects its participants directly, as [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) describes. Fluxer hands out STUN servers only and never relays media, so whether two participants connect depends on their networks. Clients report how each peer connection settled, so an operator can see which networks connect directly. + +The route on this page is user-only. Fluxer rejects a bot or OAuth2 credential with 403 `ACCESS_DENIED`. + +## Connection report object + +The outcome of one peer connection, as seen from the reporting client. A client sends one report for each remote peer of a call, once the connection to that peer reaches connected or fails. + +### Structure + +| Field | Type | Description | +| --- | --- | --- | +| channel_id1 | snowflake | The ID of the guild voice channel or private channel of the call | +| guild_id | ?snowflake | The ID of the guild of the channel, null for a call | +| participant_count | integer | Participants in the call when the connection settled (2-4) | +| outcome | string | `connected` or `failed` | +| local_candidate_type2 | ?string | Candidate type of the local end of the selected pair | +| remote_candidate_type2 | ?string | Candidate type of the remote end of the selected pair | +| ip_family3 | ?string | `ipv4` or `ipv6`, the address family of the selected pair | +| protocol3 | ?string | `udp` or `tcp`, the transport of the selected pair | +| setup_ms4 | ?integer | Milliseconds from creating the connection to its first connected state (0-2,147,483,647) | +| ice_restarted | boolean | Whether the connection ran an ICE restart | + +1 Fluxer stores the value with no check that the channel exists or that the caller was in its call + +2 One of `host`, `srflx`, `prflx`, or `relay`, and null when the connection has no selected candidate pair + +3 Null when the connection has no selected candidate pair + +4 Null for a connection that never reached connected + +### Example + +```json +{ + "channel_id": "1189375284394692610", + "guild_id": null, + "participant_count": 2, + "outcome": "connected", + "local_candidate_type": "srflx", + "remote_candidate_type": "host", + "ip_family": "ipv6", + "protocol": "udp", + "setup_ms": 840, + "ice_restarted": false +} +``` + +## Report connection outcomes + + + +Records 1 through 8 [connection report objects](#connection-report-object) and returns 204 with an empty body. User-only. Emits no Gateway event. + +A client batches its reports and sends them best effort. A failed or rate-limited request changes nothing about the call, and a client drops the batch rather than retrying more than once. + +### JSON body + +| Field | Type | Description | +| --- | --- | --- | +| reports1 | array[[connection report](#connection-report-object) object] | The reports to record (1-8) | + +1 A list of fewer than 1 or more than 8 entries, or any invalid entry, fails with 400 `INVALID_FORM_BODY` and records nothing + +### Response + +| Status | Body | Condition | +| --- | --- | --- | +| 204 | empty | Every report was recorded, or peer-to-peer voice is disabled on the instance and nothing was recorded | +| 400 | [error response](/http-api/#error-response) | The body is invalid and the request returns `INVALID_FORM_BODY` | +| 403 | [error response](/http-api/#error-response) | Caller is a bot or presents a bearer credential and the request returns `ACCESS_DENIED` | + +### Side effects + +Fluxer stores each report with the caller's user ID, the time of the request, the request address and its country, and the client platform and operating system of the caller's session. The country is null when the address resolves to none. Reports expire after 180 days. + +Fluxer keeps each report for 180 days. No route reads reports back. + +### Rate limit + +10 requests per minute for each authenticated user, on the `voice:p2p:connection_reports` bucket. diff --git a/fluxer_docs/src/content/docs/http-api/threads.mdx b/fluxer_docs/src/content/docs/http-api/threads.mdx index e36a293bd..4d711d0ae 100644 --- a/fluxer_docs/src/content/docs/http-api/threads.mdx +++ b/fluxer_docs/src/content/docs/http-api/threads.mdx @@ -54,7 +54,7 @@ A thread is a [channel object](/http-api/channels/#channel-object) with the thre 6 Present only when the caller is a member, on [Get channel](/http-api/channels/#get-channel), on [Modify channel](/http-api/channels/#modify-channel), on `thread` of a message, on [Thread Create](/gateway/threads/#thread-create), and on the `threads` list of a guild in [Ready](/gateway/events/#ready), [Guild Create](/gateway/events/#guild-create), and [Guild Sync](/gateway/events/#guild-sync) -The schema also declares `topic`, `url`, `icon`, `position`, `bitrate`, `user_limit`, `voice_connection_limit`, `rtc_region`, `permission_overwrites`, `recipients`, `nsfw`, `nsfw_override`, `content_warning_level`, `content_warning_text`, `nicks`, `default_auto_archive_duration`, `default_thread_rate_limit_per_user`, `available_tags`, `default_reaction_emoji`, `default_sort_order`, `default_forum_layout`, and `default_tag_setting`. Fluxer never sends them on a thread. A thread uses the permissions of its parent as [Thread permissions](#thread-permissions) describes. An age restriction and a content warning resolve through the parent, then the parent category, and finally the guild. +The schema also declares `topic`, `url`, `icon`, `position`, `bitrate`, `user_limit`, `voice_connection_limit`, `rtc_region`, `rtc_p2p`, `permission_overwrites`, `recipients`, `nsfw`, `nsfw_override`, `content_warning_level`, `content_warning_text`, `nicks`, `default_auto_archive_duration`, `default_thread_rate_limit_per_user`, `available_tags`, `default_reaction_emoji`, `default_sort_order`, `default_forum_layout`, and `default_tag_setting`. Fluxer never sends them on a thread. A thread uses the permissions of its parent as [Thread permissions](#thread-permissions) describes. An age restriction and a content warning resolve through the parent, then the parent category, and finally the guild. ### Example diff --git a/fluxer_docs/src/content/docs/operator/configuration.mdx b/fluxer_docs/src/content/docs/operator/configuration.mdx index 9425cbca0..1345507bd 100644 --- a/fluxer_docs/src/content/docs/operator/configuration.mdx +++ b/fluxer_docs/src/content/docs/operator/configuration.mdx @@ -744,6 +744,16 @@ Moving a media port is one line in `.env` followed by `docker compose up -d live To rotate the key pair, set both names in `.env`, then run `docker compose up -d livekit api worker` to apply the change everywhere. +### Peer-to-peer calls + +A peer-to-peer call sends media directly between its participants and not through LiveKit or any relay. It runs only while the `voice_p2p` experiment is enabled in the [instance configuration](/admin-api/instance/). The value below names the STUN servers Fluxer hands to each participant. It is optional and Compose forwards it from `.env`. The stack ships no STUN server. + +#### `FLUXER_VOICE_P2P_STUN_URLS` + +Default empty. A comma-separated list of `stun:` URLs. Participants use them to discover their public addresses. + +Unset, participants receive no ICE servers and connect only when they can already reach each other, as on one local network. Two participants that cannot reach each other directly fail to connect, and either one can switch the call to LiveKit. Participants in a peer-to-peer call can learn each other's IP addresses. + The `Caddyfile` is a bind mount, so the edge reads the copy that sits on disk beside `docker-compose.yml`. Editing it takes `docker compose restart edge`, because `docker compose up -d` leaves a container alone when only a mounted file changed. An upgrade does that restart itself, which [What the script does](/operator/upgrading/#what-the-script-does) covers. A change to any LiveKit value in `.env` needs `docker compose up -d`, because `restart` reuses the existing container with its old environment. ## Email diff --git a/fluxer_docs/src/content/docs/voice/index.md b/fluxer_docs/src/content/docs/voice/index.md index 42b259e7f..90314ffae 100644 --- a/fluxer_docs/src/content/docs/voice/index.md +++ b/fluxer_docs/src/content/docs/voice/index.md @@ -28,6 +28,8 @@ Media never crosses the HTTP API. No route returns an audio or video track, the Fluxer runs media over LiveKit and publishes no signalling protocol of its own. There is no voice websocket, no voice opcode set, no UDP discovery step, and no separate encryption handshake. A client connects to the `endpoint` the [Voice Server Update](/gateway/events/#voice-server-update) grant names, presents the grant `token`, and speaks the LiveKit protocol from there. +A peer-to-peer grant is the one exception. It has ICE servers and no `endpoint` or `token`, and the peers signal through the Gateway as [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) describes. + | Concept | Value | | --- | --- | | Room name, guild voice channel | `guild_{guild_id}_channel_{channel_id}` | @@ -67,11 +69,11 @@ A grant is issued when a connection opens, when it moves to another channel, and The grant `token` is issued for the media server and consumed by the media connection alone. No route on this API accepts it. -Fluxer reports a refusal by sending no Dispatch. A client observes a refused placement only as the absence of a grant. +Fluxer reports a refusal by sending no Dispatch. A client observes most refused placements only as the absence of a grant. A refusal with `VOICE_CHANNEL_FULL`, `VOICE_P2P_CONSENT_REQUIRED`, or `VOICE_P2P_UNAVAILABLE` also sends the requesting session a [Gateway Error](/gateway/opcodes-and-close-codes/#gateway-error) payload. ## Guild voice channels -A guild voice channel stores its `bitrate`, `user_limit`, `voice_connection_limit`, and `rtc_region` on the [channel object](/http-api/channels/#channel-object). It also has ordinary messages, pins, and slowmode, so its text history is read and written through the [Messages resource](/http-api/messages/). +A guild voice channel stores its `bitrate`, `user_limit`, `voice_connection_limit`, `rtc_region`, and `rtc_p2p` on the [channel object](/http-api/channels/#channel-object). It also has ordinary messages, pins, and slowmode, so its text history is read and written through the [Messages resource](/http-api/messages/). A new voice channel stores a `bitrate` of 64000. The ceiling is 96000, and the `AUDIO_BITRATE_128_KBPS`, `AUDIO_BITRATE_256_KBPS`, and `AUDIO_BITRATE_384_KBPS` [guild features](/http-api/guilds/#guild-features) raise it to 128000, 256000, and 384000. A direct message and a group direct message call have no `bitrate` and always run at 64000. diff --git a/fluxer_gateway/src/call/call.erl b/fluxer_gateway/src/call/call.erl index ae19d349c..ee40c22ed 100644 --- a/fluxer_gateway/src/call/call.erl +++ b/fluxer_gateway/src/call/call.erl @@ -25,6 +25,7 @@ | {stop_ringing, [integer()]} | {join, integer(), map(), binary(), pid()} | {join, integer(), map(), binary(), pid(), binary() | undefined} + | {join, integer(), map(), binary(), pid(), binary(), pos_integer()} | {confirm_connection, binary()} | {disconnect_user_if_in_channel, integer(), integer(), binary() | undefined} | {leave, binary()} @@ -33,7 +34,8 @@ | {get_pending_connections}. -type cast_request() :: {join_async, integer(), map(), binary(), pid()} - | {set_caller, map()}. + | {set_caller, map()} + | {voice_signal, binary(), binary(), map()}. -type info_message() :: {'DOWN', reference(), process, pid(), term()} | {ring_timeout, integer()} @@ -134,9 +136,11 @@ handle_call_request({ring_recipients, Recipients}, State) -> handle_call_request({stop_ringing, Recipients}, State) -> handle_stop_ringing(Recipients, State); handle_call_request({join, UserId, VS, SId, SPid}, State) -> - handle_join_request(UserId, VS, SId, SPid, undefined, State); + handle_join_request(UserId, VS, SId, SPid, undefined, undefined, State); handle_call_request({join, UserId, VS, SId, SPid, CId}, State) -> - handle_join_request(UserId, VS, SId, SPid, CId, State); + handle_join_request(UserId, VS, SId, SPid, CId, undefined, State); +handle_call_request({join, UserId, VS, SId, SPid, CId, MaxParticipants}, State) -> + handle_join_request(UserId, VS, SId, SPid, CId, MaxParticipants, State); handle_call_request({confirm_connection, ConnectionId}, State) -> handle_confirm_connection(ConnectionId, State); handle_call_request({disconnect_user_if_in_channel, UserId, ExpChId, CId}, State) -> @@ -151,14 +155,16 @@ handle_call_request({get_pending_connections}, State) -> handle_get_pending_connections(State). -spec handle_join_request( - integer(), map(), binary(), pid(), binary() | undefined, map() -) -> {reply, ok, map()}. -handle_join_request(UserId, VS, SId, SPid, CId, #{sessions := Sessions} = State) -> + integer(), map(), binary(), pid(), binary() | undefined, pos_integer() | undefined, map() +) -> {reply, ok | {error, atom()}, map()}. +handle_join_request( + UserId, VS, SId, SPid, CId, MaxParticipants, #{sessions := Sessions} = State +) -> case maps:get(SId, Sessions, undefined) of {UserId, SPid, _Ref} -> {reply, ok, State}; _ -> - call_voice:handle_join_internal(UserId, VS, SId, SPid, CId, State) + call_voice:handle_join_internal(UserId, VS, SId, SPid, CId, MaxParticipants, State) end. -spec handle_cast(term(), map()) -> {noreply, map()}. @@ -168,6 +174,9 @@ handle_cast(Request, State) -> call_voice:handle_join_async(UserId, VoiceState, SessionId, SessionPid, State); {ok, {set_caller, Caller}} -> {noreply, call_state:put_caller(Caller, State)}; + {ok, {voice_signal, SessionId, To, Data}} -> + ok = call_voice:relay_voice_signal(SessionId, To, Data, State), + {noreply, State}; error -> {noreply, State} end. @@ -269,6 +278,18 @@ decode_session_call_request( is_binary(ConnectionId) -> {ok, {join, UserId, VoiceState, SessionId, SessionPid, ConnectionId}}; +decode_session_call_request( + {join, UserId, VoiceState, SessionId, SessionPid, ConnectionId, MaxParticipants} +) when + is_integer(UserId), + is_map(VoiceState), + is_binary(SessionId), + is_pid(SessionPid), + is_binary(ConnectionId), + is_integer(MaxParticipants), + MaxParticipants >= 1 +-> + {ok, {join, UserId, VoiceState, SessionId, SessionPid, ConnectionId, MaxParticipants}}; decode_session_call_request({confirm_connection, ConnectionId}) when is_binary(ConnectionId) -> {ok, {confirm_connection, ConnectionId}}; decode_session_call_request( @@ -323,6 +344,10 @@ decode_cast_request({join_async, UserId, VoiceState, SessionId, SessionPid}) whe {ok, {join_async, UserId, VoiceState, SessionId, SessionPid}}; decode_cast_request({set_caller, Caller}) when is_map(Caller) -> {ok, {set_caller, Caller}}; +decode_cast_request({voice_signal, SessionId, To, Data}) when + is_binary(SessionId), is_binary(To), is_map(Data) +-> + {ok, {voice_signal, SessionId, To, Data}}; decode_cast_request(_) -> error. @@ -354,9 +379,15 @@ build_get_state_reply(State) -> handle_update_region(NewRegion, State) -> OldRegion = maps:get(region, State, undefined), NewState = State#{region => NewRegion}, - UpdatedState = call_ringing:dispatch_call_update(NewState), - call_voice:maybe_spawn_region_switch(OldRegion, NewRegion, UpdatedState), - {reply, ok, UpdatedState}. + case voice_p2p:channel_mode(maps:values(maps:get(voice_states, NewState))) of + p2p -> + ConvertedState = call_voice:convert_to_sfu([], NewState), + {reply, ok, call_ringing:dispatch_call_update(ConvertedState)}; + _ -> + UpdatedState = call_ringing:dispatch_call_update(NewState), + call_voice:maybe_spawn_region_switch(OldRegion, NewRegion, UpdatedState), + {reply, ok, UpdatedState} + end. -spec handle_ring_recipients([integer()], map()) -> {reply, ok, map()}. handle_ring_recipients(Recipients, State) -> @@ -398,8 +429,7 @@ handle_confirm_connection(ConnectionId, State) -> handle_update_voice_state(UserId, VoiceState, State) -> case maps:is_key(UserId, maps:get(voice_states, State)) of true -> - NewVS = (maps:get(voice_states, State))#{UserId => VoiceState}, - NewState = State#{voice_states => NewVS}, + NewState = call_voice:update_voice_state(UserId, VoiceState, State), CountedState = call_state:sync_voice_state_count_diff(State, NewState), UpdatedState = call_ringing:dispatch_call_update(CountedState), {reply, ok, UpdatedState}; diff --git a/fluxer_gateway/src/call/call_ringing.erl b/fluxer_gateway/src/call/call_ringing.erl index 966bae54a..7df905405 100644 --- a/fluxer_gateway/src/call/call_ringing.erl +++ b/fluxer_gateway/src/call/call_ringing.erl @@ -16,6 +16,7 @@ dispatch_call_create/1, dispatch_call_update/1, dispatch_call_delete/1, + dispatch_voice_state_update/2, handle_ring_timeout/2, handle_idle_timeout/1, maybe_stop_or_reply/3, @@ -251,6 +252,14 @@ dispatch_call_delete(State) -> notify_call_ended(cancel_all_ringing_timers(State)), ok. +-spec dispatch_voice_state_update(map(), map()) -> ok. +dispatch_voice_state_update(VoiceState, State) -> + Event = voice_state_utils:sanitize_voice_state_for_broadcast( + call_state:format_voice_state(VoiceState) + ), + _ = dispatch_to_recipients(maps:get(recipients, State), voice_state_update, Event), + ok. + -spec dispatch_to_recipients([integer()], atom(), map()) -> pid(). dispatch_to_recipients(Recipients, EventType, Event) -> proc_lib:spawn(fun() -> diff --git a/fluxer_gateway/src/call/call_voice.erl b/fluxer_gateway/src/call/call_voice.erl index 4fde9744b..d97c98df1 100644 --- a/fluxer_gateway/src/call/call_voice.erl +++ b/fluxer_gateway/src/call/call_voice.erl @@ -4,7 +4,7 @@ -typing([eqwalizer]). -export([ - handle_join_internal/6, + handle_join_internal/7, handle_join_async/5, handle_session_down/2, handle_disconnect_user/4, @@ -12,15 +12,44 @@ disconnect_user_after_pending_timeout/4, maybe_notify_session_force_disconnect/4, maybe_spawn_region_switch/3, + convert_to_sfu/2, + update_voice_state/3, + relay_voice_signal/4, is_session_pid_alive/1 ]). -define(PENDING_CONNECTION_TIMEOUT_MS, 30000). -spec handle_join_internal( - integer(), map(), binary(), pid(), binary() | undefined, map() -) -> {reply, ok, map()}. -handle_join_internal(UserId, VoiceState, SessionId, SessionPid, ConnectionId, State) -> + integer(), map(), binary(), pid(), binary() | undefined, pos_integer() | undefined, map() +) -> {reply, ok | {error, atom()}, map()}. +handle_join_internal( + UserId, VoiceState, SessionId, SessionPid, ConnectionId, MaxParticipants, State +) -> + case join_violation(UserId, VoiceState, MaxParticipants, State) of + {error, ErrorAtom} -> + {reply, {error, ErrorAtom}, State}; + ok -> + NewState = admit_join( + UserId, + VoiceState, + SessionId, + SessionPid, + pending_connection_id(ConnectionId, VoiceState), + State + ), + {reply, ok, NewState} + end. + +-spec join_violation(integer(), map(), pos_integer() | undefined, map()) -> + ok | {error, atom()}. +join_violation(UserId, VoiceState, MaxParticipants, #{voice_states := VoiceStates}) -> + voice_p2p:invariant_violation( + maps:values(VoiceStates#{UserId => VoiceState}), MaxParticipants + ). + +-spec admit_join(integer(), map(), binary(), pid(), binary() | undefined, map()) -> map(). +admit_join(UserId, VoiceState, SessionId, SessionPid, PendingConnectionId, State) -> CleanState = call_ringing:cancel_ringing_timers([UserId], State), BaseState = call_ringing:remove_users_from_ringing([UserId], CleanState), #{ @@ -28,23 +57,24 @@ handle_join_internal(UserId, VoiceState, SessionId, SessionPid, ConnectionId, St sessions := Sessions0, participants_history := History0 } = BaseState, - NewVoiceStates = VoiceStates0#{UserId => VoiceState}, - NewSessions = monitor_session_entry( - Sessions0, SessionId, UserId, SessionPid - ), - NewHistory = sets:add_element(UserId, History0), - NewPending = build_pending(ConnectionId, UserId, SessionId, BaseState), NewState = BaseState#{ - voice_states => NewVoiceStates, - sessions => NewSessions, - pending_connections => NewPending, - participants_history => NewHistory + voice_states => VoiceStates0#{UserId => VoiceState}, + sessions => monitor_session_entry(Sessions0, SessionId, UserId, SessionPid), + pending_connections => build_pending(PendingConnectionId, UserId, SessionId, BaseState), + participants_history => sets:add_element(UserId, History0) }, StateWithTimer = call_ringing:reset_idle_timer(NewState), {UpdatedState, Dispatched} = call_ringing:maybe_dispatch_state_update( BaseState, StateWithTimer ), - {reply, ok, ensure_call_update(UpdatedState, Dispatched)}. + ensure_call_update(UpdatedState, Dispatched). + +-spec pending_connection_id(binary() | undefined, map()) -> binary() | undefined. +pending_connection_id(ConnectionId, VoiceState) -> + case voice_p2p:is_p2p(VoiceState) of + true -> undefined; + false -> ConnectionId + end. -spec build_pending(binary() | undefined, integer(), binary(), map()) -> map(). build_pending(undefined, _UserId, _SessionId, #{pending_connections := Pending}) -> @@ -86,26 +116,17 @@ monitor_session_entry(Sessions, SessionId, UserId, SessionPid) -> handle_join_async( UserId, VoiceState, SessionId, SessionPid, #{channel_id := ChannelId} = State ) -> - CleanState = call_ringing:cancel_ringing_timers([UserId], State), - BaseState = call_ringing:remove_users_from_ringing([UserId], CleanState), - #{ - voice_states := VoiceStates0, - sessions := Sessions0, - participants_history := History0 - } = BaseState, - NewVS = VoiceStates0#{UserId => VoiceState}, - NewSess = monitor_session_entry(Sessions0, SessionId, UserId, SessionPid), - NewHistory = sets:add_element(UserId, History0), - NewState = BaseState#{ - voice_states => NewVS, sessions => NewSess, participants_history => NewHistory - }, - StateWithTimer = call_ringing:reset_idle_timer(NewState), - {UpdState, Dispatched} = call_ringing:maybe_dispatch_state_update( - BaseState, StateWithTimer - ), - FinalState = ensure_call_update(UpdState, Dispatched), - SessionPid ! {call_join_result, ChannelId, {ok, FinalState}}, - {noreply, FinalState}. + case join_violation(UserId, VoiceState, undefined, State) of + {error, ErrorAtom} -> + SessionPid ! {call_join_result, ChannelId, {error, ErrorAtom}}, + {noreply, State}; + ok -> + FinalState = admit_join( + UserId, VoiceState, SessionId, SessionPid, undefined, State + ), + SessionPid ! {call_join_result, ChannelId, {ok, FinalState}}, + {noreply, FinalState} + end. -spec maybe_remove_user_voice_state(integer(), map(), map()) -> map(). maybe_remove_user_voice_state(UserId, RemainingSessions, VoiceStates) -> @@ -285,18 +306,86 @@ maybe_spawn_region_switch_for_participants( ok end. --spec spawn_voice_server_updates(binary(), map(), map(), integer()) -> ok. +-spec spawn_voice_server_updates(binary() | undefined | null, map(), map(), integer()) -> ok. spawn_voice_server_updates(NewRegion, VoiceStates, Sessions, ChannelId) -> spawn(fun() -> send_voice_server_updates(NewRegion, VoiceStates, Sessions, ChannelId) end), ok. +-spec update_voice_state(integer(), map(), map()) -> map(). +update_voice_state(UserId, VoiceState, #{voice_states := VoiceStates} = State) -> + WasP2p = voice_p2p:is_p2p(maps:get(UserId, VoiceStates)), + case {WasP2p, voice_p2p:is_p2p(VoiceState)} of + {true, false} -> + convert_to_sfu([UserId], State#{voice_states => VoiceStates#{UserId => VoiceState}}); + {false, true} -> + Converted = voice_p2p:to_sfu(VoiceState), + NewState = State#{voice_states => VoiceStates#{UserId => Converted}}, + ok = call_ringing:dispatch_voice_state_update(Converted, NewState), + NewState; + _ -> + State#{voice_states => VoiceStates#{UserId => VoiceState}} + end. + +-spec convert_to_sfu([integer()], map()) -> map(). +convert_to_sfu( + ExtraUserIds, + #{voice_states := VoiceStates, sessions := Sessions, channel_id := ChannelId} = State +) -> + Converted = maps:map( + fun(_UserId, VoiceState) -> voice_p2p:to_sfu(VoiceState) end, + maps:filter(fun(_UserId, VoiceState) -> voice_p2p:is_p2p(VoiceState) end, VoiceStates) + ), + NewVoiceStates = maps:merge(VoiceStates, Converted), + Reconnecting = maps:with(ExtraUserIds ++ maps:keys(Converted), NewVoiceStates), + case maps:size(Reconnecting) of + 0 -> + State; + _ -> + NewState = State#{voice_states => NewVoiceStates}, + ok = dispatch_voice_state_updates(Converted, NewState), + spawn_voice_server_updates( + maps:get(region, State, undefined), Reconnecting, Sessions, ChannelId + ), + NewState + end. + +-spec dispatch_voice_state_updates(map(), map()) -> ok. +dispatch_voice_state_updates(VoiceStates, State) -> + maps:foreach( + fun(_UserId, VoiceState) -> + call_ringing:dispatch_voice_state_update(VoiceState, State) + end, + VoiceStates + ). + +-spec relay_voice_signal(binary(), binary(), map(), map()) -> ok. +relay_voice_signal( + SessionId, + To, + Data, + #{voice_states := VoiceStates, sessions := Sessions, channel_id := ChannelId} +) -> + case voice_p2p:signal_route(maps:values(VoiceStates), SessionId, To) of + {ok, Sender, Target} -> + dispatch_voice_signal( + maps:get(maps:get(<<"session_id">>, Target, undefined), Sessions, undefined), + voice_p2p:signal_payload(null, ChannelId, Sender, Data) + ); + error -> + ok + end. + +-spec dispatch_voice_signal(term(), map()) -> ok. +dispatch_voice_signal({_UserId, SessionPid, _Ref}, Payload) when is_pid(SessionPid) -> + gateway_dispatch_relay:dispatch(SessionPid, voice_signal, Payload, 0); +dispatch_voice_signal(_Session, _Payload) -> + ok. + -spec send_voice_server_updates( binary() | undefined | null, map(), map(), integer() ) -> ok. -send_voice_server_updates(NewRegion, _VS, _Sessions, _ChId) when not is_binary(NewRegion) -> - ok; send_voice_server_updates(NewRegion, VoiceStates, Sessions, ChannelId) -> maps:foreach( fun(_SessionId, {UserId, SessionPid, _Ref}) -> @@ -308,7 +397,9 @@ send_voice_server_updates(NewRegion, VoiceStates, Sessions, ChannelId) -> ), ok. --spec maybe_send_voice_server_update(integer(), pid(), binary(), integer(), map()) -> ok. +-spec maybe_send_voice_server_update( + integer(), pid(), binary() | undefined | null, integer(), map() +) -> ok. maybe_send_voice_server_update(UserId, SessionPid, NewRegion, ChannelId, VoiceStates) -> case maps:get(UserId, VoiceStates, undefined) of undefined -> @@ -317,7 +408,9 @@ maybe_send_voice_server_update(UserId, SessionPid, NewRegion, ChannelId, VoiceSt send_voice_server_update(ChannelId, UserId, SessionPid, NewRegion, VoiceState) end. --spec send_voice_server_update(integer(), integer(), pid(), binary(), map()) -> ok. +-spec send_voice_server_update( + integer(), integer(), pid(), binary() | undefined | null, map() +) -> ok. send_voice_server_update(ChannelId, UserId, SessionPid, NewRegion, VoiceState) -> ConnectionId = maps:get(<<"connection_id">>, VoiceState, null), case ConnectionId of @@ -329,16 +422,12 @@ send_voice_server_update(ChannelId, UserId, SessionPid, NewRegion, VoiceState) - Req0 = voice_utils:build_voice_token_rpc_request( null, ChannelId, UserId, ConnectionId, null, null ), - Req = voice_utils:add_rtc_region_to_request(Req0, rpc_region(NewRegion)), + Req = voice_utils:add_rtc_region_to_request( + Req0, dm_voice_state:voice_region_for_rpc(NewRegion) + ), dispatch_voice_server_rpc(ChannelId, SessionPid, Req) end. --spec rpc_region(binary()) -> binary() | null. -rpc_region(<<"automatic">>) -> - null; -rpc_region(NewRegion) -> - NewRegion. - -spec dispatch_voice_server_rpc(integer(), pid(), map()) -> ok. dispatch_voice_server_rpc(ChannelId, SessionPid, Req) -> case rpc_client:call(Req) of diff --git a/fluxer_gateway/src/gateway/gateway_errors.erl b/fluxer_gateway/src/gateway/gateway_errors.erl index d0ee0bf77..50ab88fb1 100644 --- a/fluxer_gateway/src/gateway/gateway_errors.erl +++ b/fluxer_gateway/src/gateway/gateway_errors.erl @@ -7,7 +7,8 @@ -export([ error/1, - error_category/1 + error_category/1, + voice_error_info/1 ]). -export_type([error_atom/0, error_category/0]). @@ -37,6 +38,8 @@ | voice_pending_expired | voice_camera_user_limit | voice_e2ee_required + | voice_p2p_consent_required + | voice_p2p_unavailable | dm_channel_not_found | dm_not_recipient | dm_invalid_channel_type @@ -84,7 +87,8 @@ voice_error_category(ErrorAtom) -> -spec voice_error_info(error_atom()) -> voice_error_info() | undefined. voice_error_info(ErrorAtom) -> - maps:get(ErrorAtom, voice_error_infos(), undefined). + P2pInfo = maps:get(ErrorAtom, voice_p2p_error_infos(), undefined), + maps:get(ErrorAtom, voice_error_infos(), P2pInfo). -spec voice_error_infos() -> #{error_atom() => voice_error_info()}. voice_error_infos() -> @@ -149,6 +153,18 @@ voice_error_infos() -> {<<"VOICE_E2EE_REQUIRED">>, voice_e2ee_required_message(), permission_denied} }. +-spec voice_p2p_error_infos() -> #{error_atom() => voice_error_info()}. +voice_p2p_error_infos() -> + #{ + voice_p2p_consent_required => + {<<"VOICE_P2P_CONSENT_REQUIRED">>, + <<"This call is peer-to-peer and requires agreement to join">>, + permission_denied}, + voice_p2p_unavailable => + {<<"VOICE_P2P_UNAVAILABLE">>, <<"Peer-to-peer voice is unavailable for this join">>, + voice_error} + }. + -spec voice_e2ee_required_message() -> binary(). voice_e2ee_required_message() -> iolist_to_binary([ diff --git a/fluxer_gateway/src/gateway/gateway_handler.erl b/fluxer_gateway/src/gateway/gateway_handler.erl index b01bec98b..f45612dd1 100644 --- a/fluxer_gateway/src/gateway/gateway_handler.erl +++ b/fluxer_gateway/src/gateway/gateway_handler.erl @@ -125,6 +125,8 @@ websocket_info({dispatch, Event, {pre_encoded, Bin} = Data, Seq}, State) when is_integer(Seq), is_binary(Event), is_binary(Bin) -> gateway_handler_dispatch:handle_dispatch(Event, Data, Seq, State); +websocket_info({gateway_error, ErrorAtom}, State) when is_atom(ErrorAtom) -> + gateway_handler_dispatch:handle_gateway_error(ErrorAtom, State); websocket_info(rollout_config_changed, State) -> gateway_handler_identify:handle_rollout_config_changed(State); websocket_info({retry_pending_identify, Token}, State) when is_reference(Token) -> diff --git a/fluxer_gateway/src/gateway/gateway_handler_dispatch.erl b/fluxer_gateway/src/gateway/gateway_handler_dispatch.erl index 4fe66ba97..3a28d69d7 100644 --- a/fluxer_gateway/src/gateway/gateway_handler_dispatch.erl +++ b/fluxer_gateway/src/gateway/gateway_handler_dispatch.erl @@ -6,6 +6,7 @@ -export([ handle_opcode/3, handle_dispatch/4, + handle_gateway_error/2, handle_session_down/1, handle_session_reconnect/1, handle_presence_update/3, @@ -72,6 +73,13 @@ handle_authenticated_opcode(voice_state_update, _Data, State) -> gateway_handler_encode:close_with_reason( decode_error, <<"Invalid voice payload">>, State ); +handle_authenticated_opcode(voice_signal, Data, #{session_pid := Pid} = State) when + is_pid(Pid), is_map(Data) +-> + gen_server:cast(Pid, {voice_signal, Data}), + {ok, State}; +handle_authenticated_opcode(voice_signal, _Data, State) -> + {ok, State}; handle_authenticated_opcode(request_guild_members, Data, #{session_pid := Pid} = State) when is_pid(Pid) -> @@ -125,6 +133,22 @@ do_dispatch(Event, {pre_encoded, EncodedData}, Seq, State) -> do_dispatch(Event, Data, Seq, State) -> dispatch_standard(Event, Data, Seq, State). +-spec handle_gateway_error(atom(), state()) -> ws_result(). +handle_gateway_error(ErrorAtom, State) -> + {Code, ErrorMessage, _Category} = gateway_errors:voice_error_info(ErrorAtom), + Message = #{ + <<"op">> => constants:opcode_to_num(gateway_error), + <<"d">> => #{<<"code">> => Code, <<"message">> => ErrorMessage} + }, + encode_frame(Message, State). + +-spec encode_frame(map(), state()) -> ws_result(). +encode_frame(Message, State) -> + case gateway_handler_encode:encode_and_compress(Message, State) of + {ok, Frame, NewState} -> {[Frame], NewState}; + {error, _} -> {ok, State} + end. + -spec peer_ip(state()) -> binary(). peer_ip(#{peer_ip := PeerIP}) when is_binary(PeerIP) -> PeerIP; @@ -197,10 +221,7 @@ handle_session_down(State) -> request_guild_members_pid => undefined, request_guild_members_pending => undefined }, - case gateway_handler_encode:encode_and_compress(Message, NewState) of - {ok, Frame, NewState2} -> {[Frame], NewState2}; - {error, _} -> {ok, NewState} - end. + encode_frame(Message, NewState). -spec handle_session_reconnect(state()) -> ws_result(). handle_session_reconnect(State) -> diff --git a/fluxer_gateway/src/gateway/gateway_handler_rate_limit.erl b/fluxer_gateway/src/gateway/gateway_handler_rate_limit.erl index 89ce28a74..1e68d2bba 100644 --- a/fluxer_gateway/src/gateway/gateway_handler_rate_limit.erl +++ b/fluxer_gateway/src/gateway/gateway_handler_rate_limit.erl @@ -18,6 +18,8 @@ -define(PRESENCE_RATE_LIMIT_MAX_EVENTS, 5). -define(FORUM_UNREADS_RATE_LIMIT_WINDOW_MS, 5000). -define(FORUM_UNREADS_RATE_LIMIT_MAX_EVENTS, 5). +-define(VOICE_SIGNAL_RATE_LIMIT_WINDOW_MS, 10000). +-define(VOICE_SIGNAL_RATE_LIMIT_MAX_EVENTS, 240). -define(SHARED_IP_RATE_TABLE, gateway_shared_ip_rate). -define(SHARED_USER_RATE_TABLE, gateway_shared_user_rate). @@ -109,6 +111,14 @@ check_opcode_rate_limit(State, request_forum_unreads, Now) -> ?FORUM_UNREADS_RATE_LIMIT_WINDOW_MS, ?FORUM_UNREADS_RATE_LIMIT_MAX_EVENTS ); +check_opcode_rate_limit(State, voice_signal, Now) -> + check_named_opcode_rate_limit( + State, + voice_signal, + Now, + ?VOICE_SIGNAL_RATE_LIMIT_WINDOW_MS, + ?VOICE_SIGNAL_RATE_LIMIT_MAX_EVENTS + ); check_opcode_rate_limit(State, _Op, _Now) -> {ok, State}. diff --git a/fluxer_gateway/src/guild/guild.erl b/fluxer_gateway/src/guild/guild.erl index e99e7e386..e8d333fc6 100644 --- a/fluxer_gateway/src/guild/guild.erl +++ b/fluxer_gateway/src/guild/guild.erl @@ -253,6 +253,7 @@ cast_handler(store_pending_connection) -> voice; cast_handler(add_virtual_channel_access) -> voice; cast_handler(remove_virtual_channel_access) -> voice; cast_handler(cleanup_virtual_access_for_user) -> voice; +cast_handler(voice_signal) -> voice; cast_handler(update_member_subscriptions) -> subscription; cast_handler(update_dm_partners) -> dm_partners; cast_handler(_) -> undefined. diff --git a/fluxer_gateway/src/guild/guild_client.erl b/fluxer_gateway/src/guild/guild_client.erl index 9b4edcf68..9804d84a3 100644 --- a/fluxer_gateway/src/guild/guild_client.erl +++ b/fluxer_gateway/src/guild/guild_client.erl @@ -19,7 +19,9 @@ endpoint => binary(), connection_id => binary(), voice_state => map(), - needs_token => boolean() + needs_token => boolean(), + p2p => true, + ice_servers => [map()] }. -type voice_state_update_rejection() :: #{ diff --git a/fluxer_gateway/src/guild/guild_voice_handler.erl b/fluxer_gateway/src/guild/guild_voice_handler.erl index 736f8abd3..5177843fa 100644 --- a/fluxer_gateway/src/guild/guild_voice_handler.erl +++ b/fluxer_gateway/src/guild/guild_voice_handler.erl @@ -198,7 +198,39 @@ handle_cast({cleanup_virtual_access_for_user, UId}, State) when is_integer(UId) -> NewState = guild_voice_disconnect:cleanup_virtual_channel_access_for_user(UId, State), - {noreply, NewState}. + {noreply, NewState}; +handle_cast({voice_signal, Request}, State) when is_map(Request) -> + ok = relay_voice_signal(Request, State), + {noreply, State}. + +-spec relay_voice_signal(map(), guild_state()) -> ok. +relay_voice_signal( + #{session_id := SessionId, channel_id := ChannelId, to := To, data := Data}, State +) -> + ChannelVoiceStates = maps:values( + voice_state_utils:channel_voice_states(ChannelId, voice_state_utils:voice_states(State)) + ), + case voice_p2p:signal_route(ChannelVoiceStates, SessionId, To) of + {ok, Sender, Target} -> + GuildId = maps:get(id, State), + dispatch_voice_signal( + maps:get(<<"session_id">>, Target, undefined), + voice_p2p:signal_payload(GuildId, ChannelId, Sender, Data), + GuildId, + State + ); + error -> + ok + end. + +-spec dispatch_voice_signal(term(), map(), integer(), guild_state()) -> ok. +dispatch_voice_signal(SessionId, Payload, GuildId, State) -> + case maps:get(SessionId, maps:get(sessions, State, #{}), undefined) of + #{pid := Pid} when is_pid(Pid) -> + gateway_dispatch_relay:dispatch(Pid, voice_signal, Payload, GuildId); + _ -> + ok + end. -spec cast_relay_voice_server( integer(), integer(), binary(), binary(), binary(), binary(), guild_state() diff --git a/fluxer_gateway/src/guild/voice/dm_voice_connect.erl b/fluxer_gateway/src/guild/voice/dm_voice_connect.erl index 4966a5059..9027a1121 100644 --- a/fluxer_gateway/src/guild/voice/dm_voice_connect.erl +++ b/fluxer_gateway/src/guild/voice/dm_voice_connect.erl @@ -74,6 +74,8 @@ build_connect_request( longitude => maps:get(longitude, Opts), e2ee_capable => maps:get(e2ee_capable, Opts), bot => maps:get(bot, Opts), + p2p => maps:get(p2p, Opts), + country_code => maps:get(country_code, Opts), voice_states => maps:get(dm_voice_states, State, #{}), state => State }. @@ -98,7 +100,9 @@ extract_request_opts(Request) -> latitude => maps:get(latitude, Request, null), longitude => maps:get(longitude, Request, null), e2ee_capable => maps:get(e2ee_capable, Request, false), - bot => maps:get(bot, Request, false) + bot => maps:get(bot, Request, false), + p2p => maps:get(p2p, Request, undefined), + country_code => maps:get(country_code, Request, undefined) }. -spec handle_dm_connect_or_update( @@ -245,13 +249,16 @@ commit_existing_update( state := State } = Req ) -> - UpdatedVS = build_updated_voice_state(ExistingVoiceState, Req), + OldChannelId = maps:get(<<"channel_id">>, ExistingVoiceState, null), + NeedsToken = not snowflake_id:equal(ChannelIdValue, OldChannelId), + P2p = + voice_p2p:is_p2p(ExistingVoiceState) andalso not NeedsToken andalso + not voice_p2p:declined(maps:get(p2p, Req, undefined)), + UpdatedVS = build_updated_voice_state(ExistingVoiceState, Req#{p2p => P2p}), NewVoiceStates = VoiceStates#{ConnectionId => UpdatedVS}, NewState = State#{dm_voice_states => NewVoiceStates}, _ = voice_state_counts_cache:upsert_voice_state(UpdatedVS), dm_voice_ring:broadcast_voice_state_update(ChannelIdValue, UpdatedVS, NewState), - OldChannelId = maps:get(<<"channel_id">>, ExistingVoiceState, null), - NeedsToken = not snowflake_id:equal(ChannelIdValue, OldChannelId), UserId = maps:get(user_id, Req), maybe_spawn_call_voice_state_update(NeedsToken, ChannelIdValue, UserId, UpdatedVS), dm_voice_token:maybe_spawn_join_call( @@ -294,6 +301,7 @@ build_updated_voice_state(ExistingVoiceState, Req) -> <<"is_mobile">> => maps:get(is_mobile, Req), <<"suppress">> => false, <<"viewer_stream_keys">> => maps:get(viewer_stream_keys, Req), + <<"p2p">> => maps:get(p2p, Req), <<"version">> => OldVersion + 1 }). diff --git a/fluxer_gateway/src/guild/voice/dm_voice_state.erl b/fluxer_gateway/src/guild/voice/dm_voice_state.erl index 462b32f20..42c107623 100644 --- a/fluxer_gateway/src/guild/voice/dm_voice_state.erl +++ b/fluxer_gateway/src/guild/voice/dm_voice_state.erl @@ -5,6 +5,7 @@ -export([handle_dm_disconnect/4, disconnect_voice_user/2]). -export([resolve_call_region/1, resolve_call_region/2, voice_region_for_rpc/1]). +-export([call_voice_states/1]). -export([normalize_session_id/1, resolve_effective_session_id/2]). -export([validate_dm_viewer_stream_keys/3]). -export([maybe_attach_voice_routing_metadata/3]). @@ -212,6 +213,20 @@ call_region_from_pid(CallPid, Timeout) -> null end. +-spec call_voice_states(integer()) -> [voice_state()]. +call_voice_states(ChannelId) -> + case call_manager:lookup(ChannelId) of + {ok, CallPid} -> call_voice_states_from_pid(CallPid); + _ -> [] + end. + +-spec call_voice_states_from_pid(pid()) -> [voice_state()]. +call_voice_states_from_pid(CallPid) -> + case gateway_rpc_call_lookup:safe_gen_server_call(CallPid, {get_state}, 5000) of + {ok, {ok, #{voice_states := VoiceStates}}} when is_list(VoiceStates) -> VoiceStates; + _ -> [] + end. + -spec voice_region_for_rpc(term()) -> binary() | null. voice_region_for_rpc(<<"automatic">>) -> null; voice_region_for_rpc(Region) when is_binary(Region) -> Region; diff --git a/fluxer_gateway/src/guild/voice/dm_voice_token.erl b/fluxer_gateway/src/guild/voice/dm_voice_token.erl index 938f95a8c..6cdd06e9c 100644 --- a/fluxer_gateway/src/guild/voice/dm_voice_token.erl +++ b/fluxer_gateway/src/guild/voice/dm_voice_token.erl @@ -4,7 +4,7 @@ -typing([eqwalizer]). -export([get_dm_voice_token_and_create_state/1, get_voice_token/6]). --export([join_or_create_call/5, join_or_create_call/6]). +-export([join_or_create_call/6]). -export([maybe_spawn_join_call/6, dispatch_to_session/4]). -export_type([ @@ -35,9 +35,42 @@ get_dm_voice_token_and_create_state(Req) -> "dm_voice_token_create_request: user_id=~p channel_id=~p rtc_region=~p", [UserId, ChannelId, Region] ), - case rpc_client:call(ReqWithRegion) of + case dm_join_decision(Req) of + {reject, ErrorAtom} -> + {reply, gateway_errors:error(ErrorAtom), State}; + Decision -> + request_dm_token( + voice_p2p:add_to_token_request( + ReqWithRegion, Decision, maps:get(country_code, Req, undefined) + ), + Decision, + Req + ) + end. + +-spec dm_join_decision(token_request()) -> voice_p2p:join_decision(). +dm_join_decision(Req) -> + UserId = maps:get(user_id, Req), + voice_p2p:join_decision( + voice_p2p:agreed(maps:get(p2p, Req, undefined), maps:get(bot, Req, false)), + [ + VoiceState + || VoiceState <- dm_voice_state:call_voice_states(maps:get(channel_id, Req)), + not guild_voice_state:user_matches_voice_state(VoiceState, UserId) + ], + false, + false + ). + +-spec request_dm_token(map(), voice_p2p:join_decision(), token_request()) -> + {reply, map(), dm_state()} | {reply, {error, atom(), atom()}, dm_state()}. +request_dm_token(TokenReq, Decision, Req) -> + UserId = maps:get(user_id, Req), + ChannelId = maps:get(channel_id, Req), + State = maps:get(state, Req), + case rpc_client:call(TokenReq) of {ok, Data} -> - handle_dm_token_success(Data, Req); + handle_dm_token_success(Decision, Data, Req); {error, {rpc_error, _Status, Body}} -> handle_token_rpc_error(UserId, ChannelId, Body, State); {error, Reason} -> @@ -60,8 +93,40 @@ handle_token_rpc_error(UserId, ChannelId, Body, State) -> false -> {reply, gateway_errors:error(voice_token_failed), State} end. --spec handle_dm_token_success(map(), token_request()) -> {reply, map(), dm_state()}. -handle_dm_token_success(Data, Req) -> +-spec handle_dm_token_success(voice_p2p:join_decision(), map(), token_request()) -> + {reply, map(), dm_state()} | {reply, {error, atom(), atom()}, dm_state()}. +handle_dm_token_success(Decision, Data, Req) -> + case voice_p2p:admit(Decision, voice_p2p:grant(Data)) of + {reject, ErrorAtom} -> + {reply, gateway_errors:error(ErrorAtom), maps:get(state, Req)}; + {p2p, ConnectionId, IceServers, MaxParticipants} -> + handle_dm_p2p_grant(ConnectionId, IceServers, MaxParticipants, Req); + sfu -> + handle_dm_sfu_token(Data, Req) + end. + +-spec handle_dm_p2p_grant(binary(), [map()], pos_integer(), token_request()) -> + {reply, map(), dm_state()}. +handle_dm_p2p_grant(ConnectionId, IceServers, MaxParticipants, Req) -> + UserId = maps:get(user_id, Req), + ChannelId = maps:get(channel_id, Req), + SessionId = maps:get(session_id, Req), + State = maps:get(state, Req), + EffE2EE = maps:get(e2ee_capable, Req) andalso guild_voice_e2ee:is_e2ee_enabled_for_dm(), + VoiceState = build_voice_state(Req, ConnectionId, EffE2EE, true), + NewState = store_and_broadcast(ConnectionId, ChannelId, VoiceState, State), + SessionPid = maps:get(session_pid, State), + dispatch_to_session( + SessionPid, + voice_server_update, + voice_p2p:voice_server_update(ConnectionId, ChannelId, null, IceServers), + null + ), + spawn_join_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants), + {reply, #{success => true, needs_token => false, connection_id => ConnectionId}, NewState}. + +-spec handle_dm_sfu_token(map(), token_request()) -> {reply, map(), dm_state()}. +handle_dm_sfu_token(Data, Req) -> UserId = maps:get(user_id, Req), ChannelId = maps:get(channel_id, Req), SessionId = maps:get(session_id, Req), @@ -71,7 +136,7 @@ handle_dm_token_success(Data, Req) -> Endpoint = maps:get(<<"endpoint">>, Data), ConnectionId = maps:get(<<"connectionId">>, Data), EffE2EE = E2EECapable andalso guild_voice_e2ee:is_e2ee_enabled_for_dm(), - VoiceState0 = build_voice_state(Req, ConnectionId, EffE2EE), + VoiceState0 = build_voice_state(Req, ConnectionId, EffE2EE, false), VoiceState = dm_voice_state:maybe_attach_voice_routing_metadata( VoiceState0, maps:get(<<"regionId">>, Data, undefined), @@ -82,11 +147,11 @@ handle_dm_token_success(Data, Req) -> VSUpdate = build_voice_server_update(Token, Endpoint, ChannelId, ConnectionId, E2EEKey), SessionPid = maps:get(session_pid, State), dispatch_to_session(SessionPid, voice_server_update, VSUpdate, null), - spawn_join_call(ChannelId, UserId, VoiceState, SessionId, SessionPid), + spawn_join_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, undefined), {reply, #{success => true, needs_token => false, connection_id => ConnectionId}, NewState}. --spec build_voice_state(token_request(), binary(), boolean()) -> voice_state(). -build_voice_state(Req, ConnectionId, EffE2EE) -> +-spec build_voice_state(token_request(), binary(), boolean(), boolean()) -> voice_state(). +build_voice_state(Req, ConnectionId, EffE2EE, P2p) -> voice_state_utils:complete_voice_state(#{ <<"guild_id">> => null, <<"user_id">> => integer_to_binary(maps:get(user_id, Req)), @@ -102,7 +167,8 @@ build_voice_state(Req, ConnectionId, EffE2EE) -> <<"self_stream">> => maps:get(self_stream, Req), <<"suppress">> => false, <<"viewer_stream_keys">> => maps:get(viewer_stream_keys, Req), - <<"e2ee_capable">> => EffE2EE + <<"e2ee_capable">> => EffE2EE, + <<"p2p">> => P2p }). -spec store_and_broadcast(binary(), integer(), voice_state(), dm_state()) -> dm_state(). @@ -162,17 +228,27 @@ dispatch_to_session_with_relay(SessionPid, Event, Payload, GuildId) -> error:undef -> gen_server:cast(SessionPid, {dispatch, Event, Payload}) end. --spec spawn_join_call(integer(), integer(), voice_state(), binary(), pid()) -> ok. -spawn_join_call(ChannelId, UserId, VoiceState, SessionId, SessionPid) -> +-spec spawn_join_call( + integer(), integer(), voice_state(), binary(), pid(), pos_integer() | undefined +) -> ok. +spawn_join_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants) -> spawn(fun() -> - safe_join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid) + safe_join_or_create_call( + ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants + ) end), ok. --spec safe_join_or_create_call(integer(), integer(), voice_state(), binary(), pid()) -> ok. -safe_join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid) -> +-spec safe_join_or_create_call( + integer(), integer(), voice_state(), binary(), pid(), pos_integer() | undefined +) -> ok. +safe_join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants) -> _ = shard_utils:safe_apply( - fun() -> join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid) end, + fun() -> + join_or_create_call( + ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants + ) + end, ok ), ok. @@ -187,7 +263,7 @@ maybe_spawn_join_call(true, ChannelId, UserId, VoiceState, SessionId, State) whe -> case maps:get(session_pid, State, undefined) of Pid when is_pid(Pid) -> - spawn_join_call(ChannelId, UserId, VoiceState, SessionId, Pid); + spawn_join_call(ChannelId, UserId, VoiceState, SessionId, Pid, undefined); _ -> ok end; @@ -246,21 +322,33 @@ handle_get_voice_token_ok(Data, UserId, ChannelId, SessionPid) -> }}, ok. --spec join_or_create_call(integer(), integer(), voice_state(), binary(), pid()) -> ok. -join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid) -> - join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, 10). +-spec join_or_create_call( + integer(), integer(), voice_state(), binary(), pid(), pos_integer() | undefined +) -> ok. +join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants) -> + join_or_create_call( + ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, 10 + ). -spec join_or_create_call( - integer(), integer(), voice_state(), binary(), pid(), non_neg_integer() + integer(), + integer(), + voice_state(), + binary(), + pid(), + pos_integer() | undefined, + non_neg_integer() ) -> ok. -join_or_create_call(ChannelId, UserId, VoiceState, _SessId, SessionPid, 0) -> +join_or_create_call(ChannelId, UserId, VoiceState, _SessId, SessionPid, _MaxParticipants, 0) -> logger:warning( "dm_voice_join_or_create_call_exhausted_retries: user_id=~p channel_id=~p", [UserId, ChannelId] ), rollback_ghost_voice_state(ChannelId, VoiceState, SessionPid), ok; -join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retries) -> +join_or_create_call( + ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, Retries +) -> ConnectionId = maps:get(<<"connection_id">>, VoiceState, undefined), case call_manager:lookup(ChannelId) of {ok, CallPid} -> @@ -272,10 +360,13 @@ join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retrie SessionId, SessionPid, ConnectionId, + MaxParticipants, Retries ); _ -> - retry_join(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retries) + retry_join( + ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, Retries + ) end. -spec do_join_call( @@ -286,15 +377,28 @@ join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retrie binary(), pid(), term(), + pos_integer() | undefined, non_neg_integer() ) -> ok. do_join_call( - CallPid, ChannelId, UserId, VoiceState, SessionId, SessionPid, ConnectionId, Retries + CallPid, + ChannelId, + UserId, + VoiceState, + SessionId, + SessionPid, + ConnectionId, + MaxParticipants, + Retries ) -> JoinMsg = - case ConnectionId of - undefined -> {join, UserId, VoiceState, SessionId, SessionPid}; - _ -> {join, UserId, VoiceState, SessionId, SessionPid, ConnectionId} + case {ConnectionId, MaxParticipants} of + {undefined, _} -> + {join, UserId, VoiceState, SessionId, SessionPid}; + {_, undefined} -> + {join, UserId, VoiceState, SessionId, SessionPid, ConnectionId}; + _ -> + {join, UserId, VoiceState, SessionId, SessionPid, ConnectionId, MaxParticipants} end, case gateway_rpc_call_lookup:safe_gen_server_call(CallPid, JoinMsg, 5000) of {ok, ok} -> @@ -304,12 +408,17 @@ do_join_call( [UserId, ChannelId, Retries] ), ok; + {ok, {error, ErrorAtom}} -> + gen_server:cast(SessionPid, {voice_rejected, ErrorAtom}), + rollback_ghost_voice_state(ChannelId, VoiceState, SessionPid); Error -> logger:warning( dm_join_call_failed_log_message(), [UserId, ChannelId, Retries, Error] ), - retry_join(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retries) + retry_join( + ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, Retries + ) end. -spec dm_join_call_failed_log_message() -> string(). @@ -317,14 +426,24 @@ dm_join_call_failed_log_message() -> "dm_voice_join_or_create_call_failed: user_id=~p channel_id=~p " "retries_left=~p error=~p". --spec retry_join(integer(), integer(), voice_state(), binary(), pid(), non_neg_integer()) -> ok. -retry_join(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retries) -> +-spec retry_join( + integer(), + integer(), + voice_state(), + binary(), + pid(), + pos_integer() | undefined, + non_neg_integer() +) -> ok. +retry_join(ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, Retries) -> logger:warning( "dm_voice_join_or_create_call_lookup_not_found: user_id=~p channel_id=~p retries_left=~p", [UserId, ChannelId, Retries] ), ok = gateway_retry_timer:wait(300), - join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retries - 1). + join_or_create_call( + ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, Retries - 1 + ). -spec rollback_ghost_voice_state(integer(), voice_state(), pid()) -> ok. rollback_ghost_voice_state(ChannelId, VoiceState, SessionPid) -> @@ -357,7 +476,9 @@ do_join_call_dead_pid_exhausts_without_crash_test() -> VoiceState = #{<<"connection_id">> => <<"connection">>}, ?assertEqual( ok, - do_join_call(DeadPid, 1234, 42, VoiceState, <<"session">>, self(), <<"connection">>, 1) + do_join_call( + DeadPid, 1234, 42, VoiceState, <<"session">>, self(), <<"connection">>, undefined, 1 + ) ), receive {'$gen_cast', {call_monitor, 1234, DeadPid}} -> @@ -370,7 +491,7 @@ exhausted_retries_casts_force_disconnect_test() -> VoiceState = #{<<"connection_id">> => <<"conn-x">>}, ?assertEqual( ok, - join_or_create_call(1234, 42, VoiceState, <<"session">>, self(), 0) + join_or_create_call(1234, 42, VoiceState, <<"session">>, self(), undefined, 0) ), receive {'$gen_cast', {call_force_disconnect, 1234, <<"conn-x">>}} -> @@ -382,7 +503,7 @@ exhausted_retries_casts_force_disconnect_test() -> exhausted_retries_without_connection_id_casts_undefined_test() -> ?assertEqual( ok, - join_or_create_call(1234, 42, #{}, <<"session">>, self(), 0) + join_or_create_call(1234, 42, #{}, <<"session">>, self(), undefined, 0) ), receive {'$gen_cast', {call_force_disconnect, 1234, undefined}} -> diff --git a/fluxer_gateway/src/guild/voice/guild_voice_connection_apply.erl b/fluxer_gateway/src/guild/voice/guild_voice_connection_apply.erl index cc1ba2c7c..fd13c94da 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_connection_apply.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_connection_apply.erl @@ -129,7 +129,7 @@ apply_same_channel_update(Update, ParsedViewerKey) -> ChannelIdValue = maps:get(channel_id, Update), VoicePerms = maps:get(voice_permissions, Update), Flags = guild_voice_connection_util:voice_flags_for_permissions(Context, VoicePerms), - UpdateResult = guild_voice_state:update_voice_state_data(#{ + {reply, Reply, NewState} = guild_voice_state:update_voice_state_data(#{ connection_id => maps:get(connection_id, Update), channel_id => integer_to_binary(ChannelIdValue), flags => Flags, @@ -140,4 +140,7 @@ apply_same_channel_update(Update, ParsedViewerKey) -> needs_token => false, viewer_stream_keys => ParsedViewerKey }), - UpdateResult. + case voice_p2p:declined(maps:get(p2p, Context, undefined)) of + true -> {reply, Reply, guild_voice_region:switch_to_sfu(ChannelIdValue, NewState)}; + false -> {reply, Reply, NewState} + end. diff --git a/fluxer_gateway/src/guild/voice/guild_voice_connection_confirm.erl b/fluxer_gateway/src/guild/voice/guild_voice_connection_confirm.erl index 2b4a2a80e..61c1522ee 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_connection_confirm.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_connection_confirm.erl @@ -86,7 +86,7 @@ log_pending_found(ConnectionId, PendingData) -> ). -spec activate_pending(binary(), map(), map(), guild_state()) -> - {reply, map(), guild_state()}. + {reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}. activate_pending(ConnectionId, PendingData, PendingConnections, State) -> VoiceStates = voice_state_utils:voice_states(State), VoiceState = guild_voice_connection_pending:resolve_voice_state_from_pending( @@ -102,14 +102,29 @@ activate_pending(ConnectionId, PendingData, PendingConnections, State) -> end. -spec finalize_activation(binary(), voice_state(), map(), guild_state()) -> - {reply, map(), guild_state()}. + {reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}. finalize_activation(ConnectionId, VoiceState, VoiceStates, State) -> UpdatedVoiceStates = VoiceStates#{ConnectionId => VoiceState}, - S1 = State#{voice_states => UpdatedVoiceStates}, - S2 = guild_voice_connection_util:clear_virtual_access_flags(VoiceState, S1), - ChannelIdBin = maps:get(<<"channel_id">>, VoiceState, null), - guild_voice_broadcast:broadcast_voice_state_update(VoiceState, S2, ChannelIdBin), - {reply, #{success => true}, S2}. + case channel_invariant_violation(VoiceState, UpdatedVoiceStates) of + ok -> + S1 = State#{voice_states => UpdatedVoiceStates}, + S2 = guild_voice_connection_util:clear_virtual_access_flags(VoiceState, S1), + ChannelIdBin = maps:get(<<"channel_id">>, VoiceState, null), + guild_voice_broadcast:broadcast_voice_state_update(VoiceState, S2, ChannelIdBin), + {reply, #{success => true}, S2}; + {error, ErrorAtom} -> + {reply, gateway_errors:error(ErrorAtom), State} + end. + +-spec channel_invariant_violation(voice_state(), map()) -> ok | {error, atom()}. +channel_invariant_violation(VoiceState, VoiceStates) -> + voice_p2p:invariant_violation( + maps:values( + voice_state_utils:channel_voice_states( + voice_state_utils:voice_state_channel_id(VoiceState), VoiceStates + ) + ) + ). -spec try_restore_from_recently_disconnected(binary(), guild_state()) -> {reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}. @@ -126,10 +141,20 @@ try_restore_from_recently_disconnected(ConnectionId, State) -> end. -spec restore_recently_disconnected(binary(), voice_state(), map(), guild_state()) -> - {reply, map(), guild_state()}. + {reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}. restore_recently_disconnected(ConnectionId, VoiceState, Cache, State) -> VoiceStates = voice_state_utils:voice_states(State), UpdatedVoiceStates = VoiceStates#{ConnectionId => VoiceState}, + case channel_invariant_violation(VoiceState, UpdatedVoiceStates) of + ok -> + finalize_restore(ConnectionId, VoiceState, UpdatedVoiceStates, Cache, State); + {error, ErrorAtom} -> + {reply, gateway_errors:error(ErrorAtom), State} + end. + +-spec finalize_restore(binary(), voice_state(), map(), map(), guild_state()) -> + {reply, map(), guild_state()}. +finalize_restore(ConnectionId, VoiceState, UpdatedVoiceStates, Cache, State) -> NewCache = maps:remove(ConnectionId, Cache), S0 = State#{voice_states => UpdatedVoiceStates}, S1 = S0#{recently_disconnected_voice_states => NewCache}, diff --git a/fluxer_gateway/src/guild/voice/guild_voice_connection_join.erl b/fluxer_gateway/src/guild/voice/guild_voice_connection_join.erl index 508c86cd5..7745ba308 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_connection_join.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_connection_join.erl @@ -4,7 +4,7 @@ -typing([eqwalizer]). --export([handle_new_connection/5]). +-export([handle_new_connection/5, activate_p2p_voice_state/3]). -export_type([ guild_state/0, @@ -154,36 +154,81 @@ request_token_and_build(Context, Member, ParsedViewerStreamKey, State, GuildId, TokenNonce = voice_utils:generate_token_nonce(), Latitude = maps:get(latitude, Context, undefined), Longitude = maps:get(longitude, Context, undefined), + Build = #{ + context => Context, + member => Member, + viewer_stream_keys => ParsedViewerStreamKey, + state => State, + guild_id => GuildId, + guild_id_bin => GuildIdBin, + voice_permissions => VoicePermissions, + token_nonce => TokenNonce, + latitude => Latitude, + longitude => Longitude + }, case - guild_voice_connection_token:request_voice_token( - GuildId, - ChannelIdValue, - UserId, - null, - VoicePermissions, - TokenNonce, - Latitude, - Longitude + voice_p2p:guild_join_decision( + guild_voice_connection_util:p2p_agreed(Context), ChannelIdValue, State ) of - {ok, TokenData} -> - build_new_voice_state(#{ - context => Context, - member => Member, - viewer_stream_keys => ParsedViewerStreamKey, - state => State, - guild_id => GuildId, - guild_id_bin => GuildIdBin, - token_data => TokenData, - voice_permissions => VoicePermissions, - token_nonce => TokenNonce, - latitude => Latitude, - longitude => Longitude - }); - {error, _Reason} -> - {reply, gateway_errors:error(voice_token_failed), State} + {reject, ErrorAtom} -> + {reply, gateway_errors:error(ErrorAtom), State}; + Decision -> + TokenResult = guild_voice_connection_token:request_voice_token( + GuildId, + ChannelIdValue, + UserId, + null, + VoicePermissions, + TokenNonce, + Latitude, + Longitude, + Decision, + maps:get(country_code, Context, undefined) + ), + build_join_reply(Decision, TokenResult, Build) end. +-spec build_join_reply(voice_p2p:join_decision(), {ok, term()} | {error, term()}, map()) -> + {reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}. +build_join_reply(_Decision, {error, _Reason}, #{state := State}) -> + {reply, gateway_errors:error(voice_token_failed), State}; +build_join_reply(Decision, {ok, TokenData}, #{state := State} = Build) -> + case voice_p2p:admit(Decision, TokenData) of + {reject, ErrorAtom} -> + {reply, gateway_errors:error(ErrorAtom), State}; + {p2p, ConnectionId, IceServers, _MaxParticipants} -> + VoiceBuild = voice_build_fields(Build#{token_data => #{}}), + activate_p2p_voice_state( + create_and_decorate_voice_state(VoiceBuild#{connection_id => ConnectionId}), + IceServers, + State + ); + sfu -> + build_new_voice_state(Build#{token_data => TokenData}) + end. + +-spec activate_p2p_voice_state(voice_state(), [map()], guild_state()) -> + {reply, map(), guild_state()}. +activate_p2p_voice_state(VoiceState0, IceServers, State) -> + VoiceState = VoiceState0#{<<"p2p">> => true}, + ConnectionId = maps:get(<<"connection_id">>, VoiceState), + VoiceStates = voice_state_utils:voice_states(State), + S1 = State#{voice_states => VoiceStates#{ConnectionId => VoiceState}}, + S2 = guild_voice_connection_util:clear_virtual_access_flags(VoiceState, S1), + guild_voice_broadcast:broadcast_voice_state_update( + VoiceState, S2, maps:get(<<"channel_id">>, VoiceState) + ), + {reply, + #{ + success => true, + p2p => true, + ice_servers => IceServers, + connection_id => ConnectionId, + voice_state => voice_state_utils:external_voice_state(VoiceState) + }, + S2}. + -spec build_new_voice_state( map() ) -> {reply, map(), guild_state()}. diff --git a/fluxer_gateway/src/guild/voice/guild_voice_connection_move.erl b/fluxer_gateway/src/guild/voice/guild_voice_connection_move.erl index 68566ee97..a6aa466ca 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_connection_move.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_connection_move.erl @@ -66,10 +66,7 @@ do_request_move_token(Context, ChannelIdValue, Member, SessionId, MoveState, Gui TokenNonce = voice_utils:generate_token_nonce(), Lat = maps:get(latitude, Context, undefined), Lng = maps:get(longitude, Context, undefined), - TokenResult = guild_voice_connection_token:request_voice_token( - GuildId, ChannelIdValue, UserId, ConnId, VoicePerms, TokenNonce, Lat, Lng - ), - handle_move_token_result(TokenResult, #{ + Build = #{ context => Context, channel_id => ChannelIdValue, member => Member, @@ -83,18 +80,60 @@ do_request_move_token(Context, ChannelIdValue, Member, SessionId, MoveState, Gui token_nonce => TokenNonce, latitude => Lat, longitude => Lng - }). + }, + case + voice_p2p:guild_join_decision( + guild_voice_connection_util:p2p_agreed(Context), ChannelIdValue, State + ) + of + {reject, ErrorAtom} -> + move_error_reply(ErrorAtom, Build); + Decision -> + TokenResult = guild_voice_connection_token:request_voice_token( + GuildId, + ChannelIdValue, + UserId, + ConnId, + VoicePerms, + TokenNonce, + Lat, + Lng, + Decision, + maps:get(country_code, Context, undefined) + ), + handle_move_token_result(Decision, TokenResult, Build) + end. -spec handle_move_token_result( - {ok, map()} | {error, term()}, map() + voice_p2p:join_decision(), {ok, term()} | {error, term()}, map() ) -> {reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}. -handle_move_token_result({ok, TokenData}, Build) -> - build_move_result(Build#{token_data => TokenData}); -handle_move_token_result({error, _Reason}, Build) -> - #{state := State, context := Context, channel_id := ChannelIdValue} = Build, +handle_move_token_result(_Decision, {error, _Reason}, Build) -> + move_error_reply(voice_token_failed, Build); +handle_move_token_result(Decision, {ok, TokenData}, Build) -> + case voice_p2p:admit(Decision, TokenData) of + {reject, ErrorAtom} -> + move_error_reply(ErrorAtom, Build); + {p2p, ConnectionId, IceServers, _MaxParticipants} -> + build_p2p_move_result(ConnectionId, IceServers, Build#{token_data => #{}}); + sfu -> + build_move_result(Build#{token_data => TokenData}) + end. + +-spec move_error_reply(atom(), map()) -> {reply, {error, atom(), atom()}, guild_state()}. +move_error_reply(ErrorAtom, #{state := State, context := Context, channel_id := ChannelIdValue}) -> UserId = maps:get(user_id, Context), - {reply, gateway_errors:error(voice_token_failed), - clear_move_flags(UserId, ChannelIdValue, State)}. + {reply, gateway_errors:error(ErrorAtom), clear_move_flags(UserId, ChannelIdValue, State)}. + +-spec build_p2p_move_result(binary(), [map()], map()) -> {reply, map(), guild_state()}. +build_p2p_move_result(ConnectionId, IceServers, Build) -> + State1 = disconnect_old_connection(Build), + State1Cleaned = cleanup_stale_virtual_access(Build, State1), + MoveBuild = move_build_fields(Build), + guild_voice_connection_join:activate_p2p_voice_state( + build_move_voice_state(MoveBuild#{connection_id => ConnectionId}), + IceServers, + State1Cleaned + ). -spec build_move_result( map() diff --git a/fluxer_gateway/src/guild/voice/guild_voice_connection_token.erl b/fluxer_gateway/src/guild/voice/guild_voice_connection_token.erl index 06ac6057e..4c921200f 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_connection_token.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_connection_token.erl @@ -5,6 +5,7 @@ -typing([eqwalizer]). -export([request_voice_token/8]). +-export([request_voice_token/10]). -spec request_voice_token( integer(), @@ -26,15 +27,62 @@ request_voice_token( Latitude, Longitude ) -> - Req = voice_utils:build_voice_token_rpc_request( - GuildId, - ChannelId, - UserId, - ConnectionId, - Latitude, - Longitude, - VoicePermissions, - TokenNonce + case + request_voice_token( + GuildId, + ChannelId, + UserId, + ConnectionId, + VoicePermissions, + TokenNonce, + Latitude, + Longitude, + sfu, + undefined + ) + of + {ok, TokenData} when is_map(TokenData) -> {ok, TokenData}; + {ok, _Grant} -> {error, voice_token_failed}; + {error, Reason} -> {error, Reason} + end. + +-spec request_voice_token( + integer(), + integer(), + integer(), + binary() | null, + map(), + binary() | null, + binary() | undefined | null, + binary() | undefined | null, + voice_p2p:join_decision(), + binary() | undefined +) -> {ok, map() | voice_p2p:token_grant()} | {error, term()}. +request_voice_token( + GuildId, + ChannelId, + UserId, + ConnectionId, + VoicePermissions, + TokenNonce, + Latitude, + Longitude, + Decision, + CountryCode +) -> + Req = voice_p2p:add_to_token_request( + voice_utils:build_voice_token_rpc_request( + GuildId, + ChannelId, + UserId, + ConnectionId, + Latitude, + Longitude, + VoicePermissions, + TokenNonce + ), + Decision, + CountryCode ), log_token_start(GuildId, ChannelId, UserId, ConnectionId, TokenNonce), case rpc_client:call(Req) of @@ -58,8 +106,16 @@ token_start_log_message() -> "guild_voice_request_token_start: guild_id=~p channel_id=~p " "user_id=~p connection_id=~p token_nonce=~p". --spec handle_token_success(integer(), integer(), integer(), map()) -> {ok, map()}. +-spec handle_token_success(integer(), integer(), integer(), map()) -> + {ok, map() | voice_p2p:token_grant()}. handle_token_success(GuildId, ChannelId, UserId, Data) -> + case voice_p2p:grant(Data) of + sfu -> handle_sfu_token_success(GuildId, ChannelId, UserId, Data); + Grant -> {ok, Grant} + end. + +-spec handle_sfu_token_success(integer(), integer(), integer(), map()) -> {ok, map()}. +handle_sfu_token_success(GuildId, ChannelId, UserId, Data) -> logger:debug( token_success_log_message(), [ diff --git a/fluxer_gateway/src/guild/voice/guild_voice_connection_util.erl b/fluxer_gateway/src/guild/voice/guild_voice_connection_util.erl index ed14f0f03..d25f1e3ad 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_connection_util.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_connection_util.erl @@ -20,6 +20,7 @@ normalize_session_id/1, normalize_optional_binary/1, maybe_error_reply/5, + p2p_agreed/1, clear_virtual_access_flags/2 ]). @@ -61,9 +62,15 @@ build_context(Request0) -> latitude => Coord(maps:get(latitude, Request, undefined)), longitude => Coord(maps:get(longitude, Request, undefined)), e2ee_capable => Norm(maps:get(e2ee_capable, Request, false)), - bot => Norm(maps:get(bot, Request, false)) + bot => Norm(maps:get(bot, Request, false)), + p2p => maps:get(p2p, Request, undefined), + country_code => maps:get(country_code, Request, undefined) }. +-spec p2p_agreed(context()) -> boolean(). +p2p_agreed(Context) -> + voice_p2p:agreed(maps:get(p2p, Context, undefined), maps:get(bot, Context, false)). + -spec voice_flags_for_permissions(context(), voice_utils:voice_permissions()) -> voice_flags(). voice_flags_for_permissions(Context, VoicePermissions) -> voice_utils:apply_voice_permissions_to_flags( diff --git a/fluxer_gateway/src/guild/voice/guild_voice_move_execute.erl b/fluxer_gateway/src/guild/voice/guild_voice_move_execute.erl index a64145252..e7311e0e0 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_move_execute.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_move_execute.erl @@ -160,10 +160,17 @@ check_perms_and_execute(Conns, ChannelId, UserId, ModId, VoiceStates, State) -> ModPerms = guild_permissions:get_member_permissions(ModId, ChannelId, State), ModHasConnect = permission_bits:has(ModPerms, ConnectPerm), ModHasView = permission_bits:has(ModPerms, ViewPerm), - case ModHasConnect andalso ModHasView of - false -> + case + { + ModHasConnect andalso ModHasView, + voice_p2p:guild_join_decision(false, ChannelId, State) + } + of + {false, _Decision} -> {reply, gateway_errors:error(voice_moderator_missing_connect), State}; - true -> + {true, {reject, ErrorAtom}} -> + {reply, gateway_errors:error(ErrorAtom), State}; + {true, _Decision} -> execute_move(Conns, ChannelId, UserId, VoiceStates, State) end. diff --git a/fluxer_gateway/src/guild/voice/guild_voice_region.erl b/fluxer_gateway/src/guild/voice/guild_voice_region.erl index 5f6deb9ad..ccb6c774f 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_region.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_region.erl @@ -5,6 +5,7 @@ -export([switch_voice_region_handler/2]). -export([switch_voice_region/3]). +-export([switch_to_sfu/2]). -export_type([ guild_state/0, @@ -28,16 +29,60 @@ switch_voice_region_handler(Request, State) -> undefined -> {reply, gateway_errors:error(voice_channel_not_found), State}; _ -> - switch_voice_region_channel_reply(Channel, State) + switch_voice_region_channel_reply(Channel, ChannelId, State) end. --spec switch_voice_region_channel_reply(map(), guild_state()) -> guild_reply(map()). -switch_voice_region_channel_reply(Channel, State) -> +-spec switch_voice_region_channel_reply(map(), integer(), guild_state()) -> guild_reply(map()). +switch_voice_region_channel_reply(Channel, ChannelId, State) -> case map_utils:get_integer(Channel, <<"type">>, undefined) of - 2 -> {reply, #{success => true}, State}; + 2 -> {reply, #{success => true}, convert_to_sfu(Channel, ChannelId, State)}; _ -> {reply, gateway_errors:error(voice_channel_not_voice), State} end. +-spec switch_to_sfu(integer(), guild_state()) -> guild_state(). +switch_to_sfu(ChannelId, State) -> + Channel = guild_voice_member:find_channel_by_id(ChannelId, State), + NewState = convert_to_sfu(Channel, ChannelId, State), + ok = spawn_switch_if_converted(ChannelId, State, NewState), + NewState. + +-spec spawn_switch_if_converted(integer(), guild_state(), guild_state()) -> ok. +spawn_switch_if_converted(_ChannelId, State, State) -> + ok; +spawn_switch_if_converted(ChannelId, _State, #{id := GuildId} = NewState) -> + GuildPid = maps:get(guild_pid, NewState, self()), + spawn(fun() -> switch_voice_region(GuildId, ChannelId, GuildPid) end), + ok. + +-spec convert_to_sfu(map() | undefined, integer(), guild_state()) -> guild_state(). +convert_to_sfu(Channel, ChannelId, State) -> + VoiceStates = voice_state_utils:voice_states(State), + P2pVoiceStates = maps:filter( + fun(_ConnectionId, VoiceState) -> voice_p2p:is_p2p(VoiceState) end, + voice_state_utils:channel_voice_states(ChannelId, VoiceStates) + ), + case voice_p2p:pinned(Channel) orelse maps:size(P2pVoiceStates) =:= 0 of + true -> State; + false -> mark_voice_states_sfu(P2pVoiceStates, VoiceStates, State) + end. + +-spec mark_voice_states_sfu(map(), map(), guild_state()) -> guild_state(). +mark_voice_states_sfu(P2pVoiceStates, VoiceStates, State) -> + Converted = maps:map( + fun(_ConnectionId, VoiceState) -> voice_p2p:to_sfu(VoiceState) end, + P2pVoiceStates + ), + NewState = State#{voice_states => maps:merge(VoiceStates, Converted)}, + maps:foreach( + fun(_ConnectionId, VoiceState) -> + guild_voice_broadcast:broadcast_voice_state_update( + VoiceState, NewState, maps:get(<<"channel_id">>, VoiceState, null) + ) + end, + Converted + ), + NewState. + -spec switch_voice_region(integer(), integer(), pid()) -> ok. switch_voice_region(GuildId, ChannelId, GuildPid) -> case guild_voice_server_state:guild_state_call(GuildPid, 10000) of @@ -103,10 +148,12 @@ collect_user_in_channel(ConnectionId, VoiceState, ChannelId, Acc) -> -spec collect_user_voice_state(binary(), voice_state(), list()) -> list(). collect_user_voice_state(ConnectionId, VoiceState, Acc) -> - case voice_state_utils:voice_state_user_id(VoiceState) of - undefined -> + case {voice_p2p:is_p2p(VoiceState), voice_state_utils:voice_state_user_id(VoiceState)} of + {true, _UserId} -> Acc; - UserId -> + {false, undefined} -> + Acc; + {false, UserId} -> SessionId = maps:get(<<"session_id">>, VoiceState, undefined), [{UserId, SessionId, ConnectionId, VoiceState} | Acc] end. diff --git a/fluxer_gateway/src/guild/voice/guild_voice_server.erl b/fluxer_gateway/src/guild/voice/guild_voice_server.erl index dddacb69c..a13d836bd 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_server.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_server.erl @@ -124,6 +124,7 @@ init(#{guild_id := GuildId, guild_pid := GuildPid} = Args) -> guild_voice_server_sync:ensure_registry(), ets:insert(?REGISTRY_TABLE, {GuildId, self()}), erlang:send_after(?SWEEP_INTERVAL_MS, self(), sweep_pending_joins), + erlang:send_after(?SEEDED_SESSION_CHECK_DELAY_MS, self(), check_p2p_sessions), InitialVoiceStates = voice_state_utils:ensure_voice_states( maps:get(initial_voice_states, Args, #{}) ), @@ -254,6 +255,12 @@ handle_info(sweep_pending_joins, State) -> NewGuildState = guild_voice_connection:sweep_expired_pending_joins(GuildState), {noreply, guild_voice_server_state:apply_guild_state(NewGuildState, State2)} end; +handle_info(check_p2p_sessions, State) -> + erlang:send_after(?SEEDED_SESSION_CHECK_DELAY_MS, self(), check_p2p_sessions), + ok = spawn_seeded_session_check( + first, seeded_sessions(p2p_connection_ids(State), State), State + ), + {noreply, State}; handle_info({check_seeded_sessions, Round, ConnectionIds}, State) -> ok = spawn_seeded_session_check(Round, seeded_sessions(ConnectionIds, State), State), {noreply, State}; @@ -313,6 +320,15 @@ schedule_seeded_session_check(Round, ConnectionIds) -> ), ok. +-spec p2p_connection_ids(server_state()) -> [binary()]. +p2p_connection_ids(State) -> + maps:keys( + maps:filter( + fun(_ConnectionId, VoiceState) -> voice_p2p:is_p2p(VoiceState) end, + maps:get(voice_states, State, #{}) + ) + ). + -spec seeded_sessions([binary()], server_state()) -> #{binary() => {integer(), binary()}}. seeded_sessions(ConnectionIds, State) -> VoiceStates = maps:get(voice_states, State, #{}), @@ -575,6 +591,24 @@ seeded_session_check_reports_only_gone_sessions_test() -> after 1000 -> ?assert(false) end. +p2p_session_check_reports_only_gone_p2p_sessions_test() -> + State0 = seeded_test_state(self()), + #{<<"ghost">> := Ghost, <<"live">> := Live} = VoiceStates = maps:get(voice_states, State0), + State = State0#{ + voice_states => VoiceStates#{ + <<"ghost">> => Ghost#{<<"p2p">> => true}, + <<"live">> => Live#{<<"p2p">> => true}, + <<"sfu-ghost">> => Ghost#{<<"connection_id">> => <<"sfu-ghost">>} + }, + test_session_gone_fun => fun(SessionId) -> SessionId =:= <<"dead">> end + }, + {noreply, State} = handle_info(check_p2p_sessions, State), + receive + {seeded_sessions_gone, first, Gone} -> + ?assertEqual(#{<<"ghost">> => {5, <<"dead">>}}, Gone) + after 1000 -> ?assert(false) + end. + gone_seeded_connection_is_removed_test() -> TestFun = fun(_, _, _, _) -> {ok, #{success => true}} end, GuildPid = spawn(fun() -> guild_state_reply_loop(TestFun) end), diff --git a/fluxer_gateway/src/guild/voice/voice_p2p.erl b/fluxer_gateway/src/guild/voice/voice_p2p.erl new file mode 100644 index 000000000..acf90adb8 --- /dev/null +++ b/fluxer_gateway/src/guild/voice/voice_p2p.erl @@ -0,0 +1,221 @@ +%% SPDX-License-Identifier: AGPL-3.0-or-later + +-module(voice_p2p). +-typing([eqwalizer]). + +-export([ + agreed/2, + declined/1, + is_p2p/1, + pinned/1, + channel_mode/1, + join_decision/4, + guild_join_decision/3, + admit/2, + invariant_violation/1, + invariant_violation/2, + reports_rejection/1, + to_sfu/1, + add_to_token_request/3, + grant/1, + voice_server_update/4, + signal_route/3, + signal_payload/4 +]). + +-export_type([ + voice_state/0, channel_mode/0, rejection/0, join_decision/0, grant/0, token_grant/0 +]). + +-define(MAX_PARTICIPANTS, 4). + +-type voice_state() :: map(). +-type channel_mode() :: empty | p2p | sfu | mixed. +-type rejection() :: {reject, atom()}. +-type join_decision() :: {p2p, initiator | joiner, pos_integer()} | sfu | rejection(). +-type grant() :: {p2p, binary(), [map()], pos_integer()}. +-type token_grant() :: grant() | {declined, voice_channel_full}. + +-spec agreed(term(), term()) -> boolean(). +agreed(true, true) -> false; +agreed(true, _Bot) -> true; +agreed(_P2p, _Bot) -> false. + +-spec declined(term()) -> boolean(). +declined(false) -> true; +declined(_) -> false. + +-spec is_p2p(voice_state()) -> boolean(). +is_p2p(#{<<"p2p">> := true}) -> true; +is_p2p(_VoiceState) -> false. + +-spec pinned(map() | undefined) -> boolean(). +pinned(#{<<"rtc_p2p">> := true}) -> true; +pinned(_Channel) -> false. + +-spec channel_mode([voice_state()]) -> channel_mode(). +channel_mode([]) -> + empty; +channel_mode(VoiceStates) -> + case {lists:all(fun is_p2p/1, VoiceStates), lists:any(fun is_p2p/1, VoiceStates)} of + {true, _} -> p2p; + {false, true} -> mixed; + {false, false} -> sfu + end. + +-spec join_decision(boolean(), [voice_state()], boolean(), boolean()) -> join_decision(). +join_decision(Agreed, VoiceStates, HasPendingJoins, Pinned) -> + case {channel_mode(VoiceStates), HasPendingJoins} of + {p2p, _} -> join_p2p_decision(Agreed, length(VoiceStates) + 1); + {empty, false} -> start_decision(Agreed, Pinned); + _ -> sfu + end. + +-spec join_p2p_decision(boolean(), pos_integer()) -> join_decision(). +join_p2p_decision(false, _ParticipantCount) -> + {reject, voice_p2p_consent_required}; +join_p2p_decision(true, ParticipantCount) when ParticipantCount > ?MAX_PARTICIPANTS -> + {reject, voice_channel_full}; +join_p2p_decision(true, ParticipantCount) -> + {p2p, joiner, ParticipantCount}. + +-spec start_decision(boolean(), boolean()) -> join_decision(). +start_decision(true, _Pinned) -> {p2p, initiator, 1}; +start_decision(false, _Pinned) -> sfu. + +-spec guild_join_decision(boolean(), integer(), map()) -> join_decision(). +guild_join_decision(Agreed, ChannelId, State) -> + ChannelVoiceStates = maps:values( + voice_state_utils:channel_voice_states(ChannelId, voice_state_utils:voice_states(State)) + ), + HasPendingJoins = lists:any( + fun(Pending) -> maps:get(channel_id, Pending, undefined) =:= ChannelId end, + maps:values(guild_voice_connection_pending:pending_voice_connections(State)) + ), + Pinned = pinned(guild_voice_member:find_channel_by_id(ChannelId, State)), + join_decision(Agreed, ChannelVoiceStates, HasPendingJoins, Pinned). + +-spec admit(join_decision(), token_grant() | term()) -> grant() | sfu | rejection(). +admit({p2p, _Role, _Count}, {p2p, _ConnectionId, _IceServers, _MaxParticipants} = Grant) -> + Grant; +admit({p2p, _Role, _Count}, {declined, ErrorAtom}) -> + {reject, ErrorAtom}; +admit({p2p, initiator, _Count}, _Token) -> + sfu; +admit({p2p, joiner, _Count}, _Token) -> + {reject, voice_p2p_unavailable}; +admit(sfu, {p2p, _ConnectionId, _IceServers, _MaxParticipants}) -> + {reject, voice_token_failed}; +admit(sfu, {declined, _ErrorAtom}) -> + {reject, voice_token_failed}; +admit(sfu, _Token) -> + sfu. + +-spec invariant_violation([voice_state()]) -> ok | {error, atom()}. +invariant_violation(VoiceStates) -> + invariant_violation(VoiceStates, undefined). + +-spec invariant_violation([voice_state()], term()) -> ok | {error, atom()}. +invariant_violation(VoiceStates, MaxParticipants) -> + case channel_mode(VoiceStates) of + mixed -> + {error, voice_p2p_consent_required}; + p2p -> + case length(VoiceStates) > participant_cap(MaxParticipants) of + true -> {error, voice_channel_full}; + false -> ok + end; + _ -> + ok + end. + +-spec participant_cap(term()) -> pos_integer(). +participant_cap(MaxParticipants) when + is_integer(MaxParticipants), MaxParticipants >= 1, MaxParticipants =< ?MAX_PARTICIPANTS +-> + MaxParticipants; +participant_cap(_MaxParticipants) -> + ?MAX_PARTICIPANTS. + +-spec reports_rejection(term()) -> boolean(). +reports_rejection(voice_p2p_consent_required) -> true; +reports_rejection(voice_p2p_unavailable) -> true; +reports_rejection(voice_channel_full) -> true; +reports_rejection(_) -> false. + +-spec to_sfu(voice_state()) -> voice_state(). +to_sfu(VoiceState) -> + VoiceState#{ + <<"p2p">> => false, + <<"version">> => voice_state_utils:voice_state_version(VoiceState) + 1 + }. + +-spec add_to_token_request(map(), join_decision(), binary() | undefined) -> map(). +add_to_token_request(Request, {p2p, Role, ParticipantCount}, CountryCode) -> + WithP2p = Request#{ + <<"p2p">> => true, + <<"p2p_initiator">> => Role =:= initiator, + <<"p2p_participant_count">> => ParticipantCount + }, + case CountryCode of + Code when is_binary(Code) -> WithP2p#{<<"country_code">> => Code}; + _ -> WithP2p + end; +add_to_token_request(Request, _Decision, _CountryCode) -> + Request. + +-spec grant(map()) -> token_grant() | sfu. +grant( + #{<<"p2p">> := true, <<"connectionId">> := ConnectionId, <<"iceServers">> := IceServers} = + Data +) when + is_binary(ConnectionId), is_list(IceServers) +-> + {p2p, ConnectionId, + [ + maps:with([<<"urls">>], Server) + || Server <- IceServers, is_map(Server) + ], + participant_cap(maps:get(<<"maxParticipants">>, Data, undefined))}; +grant(#{<<"p2p">> := false, <<"declineReason">> := <<"channel_full">>}) -> + {declined, voice_channel_full}; +grant(_Data) -> + sfu. + +-spec voice_server_update(binary(), integer(), integer() | null, [map()]) -> map(). +voice_server_update(ConnectionId, ChannelId, GuildId, IceServers) -> + Update = #{ + <<"p2p">> => true, + <<"ice_servers">> => IceServers, + <<"connection_id">> => ConnectionId, + <<"channel_id">> => integer_to_binary(ChannelId) + }, + put_guild_id(Update, GuildId). + +-spec signal_route([voice_state()], binary(), binary()) -> + {ok, voice_state(), voice_state()} | error. +signal_route(VoiceStates, SessionId, To) -> + Peers = [VoiceState || VoiceState <- VoiceStates, is_p2p(VoiceState)], + Senders = [ + Peer + || Peer <- Peers, maps:get(<<"session_id">>, Peer, undefined) =:= SessionId + ], + Targets = [Peer || Peer <- Peers, maps:get(<<"connection_id">>, Peer, undefined) =:= To], + case {Senders, Targets} of + {[Sender | _], [Target | _]} when Sender =/= Target -> {ok, Sender, Target}; + _ -> error + end. + +-spec signal_payload(integer() | null, integer(), voice_state(), map()) -> map(). +signal_payload(GuildId, ChannelId, Sender, Data) -> + Payload = #{ + <<"channel_id">> => integer_to_binary(ChannelId), + <<"from">> => maps:get(<<"connection_id">>, Sender), + <<"user_id">> => maps:get(<<"user_id">>, Sender), + <<"data">> => Data + }, + put_guild_id(Payload, GuildId). + +-spec put_guild_id(map(), integer() | null) -> map(). +put_guild_id(Payload, null) -> Payload; +put_guild_id(Payload, GuildId) -> Payload#{<<"guild_id">> => integer_to_binary(GuildId)}. diff --git a/fluxer_gateway/src/guild/voice/voice_state_utils.erl b/fluxer_gateway/src/guild/voice/voice_state_utils.erl index 51984bc76..1e733b7ed 100644 --- a/fluxer_gateway/src/guild/voice/voice_state_utils.erl +++ b/fluxer_gateway/src/guild/voice/voice_state_utils.erl @@ -12,6 +12,7 @@ voice_state_channel_id/1, voice_state_guild_id/1, voice_state_version/1, + channel_voice_states/2, initial_voice_state_version/0, complete_voice_state/1, external_voice_state/1, @@ -81,6 +82,13 @@ voice_state_channel_id(VoiceState) -> voice_state_guild_id(VoiceState) -> snowflake_field(VoiceState, <<"guild_id">>). +-spec channel_voice_states(integer() | undefined, voice_state_map()) -> voice_state_map(). +channel_voice_states(ChannelId, VoiceStates) -> + maps:filter( + fun(_ConnId, VoiceState) -> voice_state_channel_id(VoiceState) =:= ChannelId end, + VoiceStates + ). + -spec voice_state_version(voice_state()) -> non_neg_integer(). voice_state_version(VoiceState) -> case map_utils:get_integer(VoiceState, <<"version">>, undefined) of @@ -117,6 +125,7 @@ default_voice_state_fields() -> <<"suppress">> => false, <<"viewer_stream_keys">> => [], <<"e2ee_capable">> => false, + <<"p2p">> => false, <<"region_id">> => null, <<"server_id">> => null, <<"version">> => ?INITIAL_VOICE_STATE_VERSION @@ -389,6 +398,7 @@ complete_voice_state_fills_missing_fields_test() -> ?assertEqual(false, maps:get(<<"suppress">>, Completed)), ?assertEqual([], maps:get(<<"viewer_stream_keys">>, Completed)), ?assertEqual(false, maps:get(<<"e2ee_capable">>, Completed)), + ?assertEqual(false, maps:get(<<"p2p">>, Completed)), ?assertEqual(null, maps:get(<<"region_id">>, Completed)), ?assertEqual(null, maps:get(<<"server_id">>, Completed)), ?assertEqual(0, maps:get(<<"version">>, Completed)). diff --git a/fluxer_gateway/src/session/session.erl b/fluxer_gateway/src/session/session.erl index 833614c3e..5b798091b 100644 --- a/fluxer_gateway/src/session/session.erl +++ b/fluxer_gateway/src/session/session.erl @@ -55,6 +55,7 @@ bot => boolean(), shard => gateway_sharding:shard() | undefined, e2ee_capable => boolean(), + geoip_country_code => binary() | undefined, ignored_events => #{binary() => true}, initial_guild_id => guild_id() | undefined, collected_guild_states => [map()], @@ -146,6 +147,12 @@ handle_cast({dispatch, Event, Data}, State) when session_dispatch:handle_dispatch(Event, Data, State); handle_cast({initial_global_presences, Presences}, State) -> handle_cast_presences(Presences, State); +handle_cast({voice_rejected, ErrorAtom}, State) when is_atom(ErrorAtom) -> + ok = session_voice:report_rejection(ErrorAtom, State), + {noreply, State}; +handle_cast({voice_signal, Data}, State) when is_map(Data) -> + ok = session_voice:handle_voice_signal(Data, State), + {noreply, State}; handle_cast(Msg, State) -> handle_cast_guild_or_lifecycle(Msg, State). diff --git a/fluxer_gateway/src/session/session_init.erl b/fluxer_gateway/src/session/session_init.erl index cf6f80005..745398e1c 100644 --- a/fluxer_gateway/src/session/session_init.erl +++ b/fluxer_gateway/src/session/session_init.erl @@ -246,7 +246,8 @@ extract_core_fields( bot => Bot, shard => maps:get(shard, D, undefined), is_staff => IsStaff, - e2ee_capable => maps:get(e2ee_capable, D, false) + e2ee_capable => maps:get(e2ee_capable, D, false), + geoip_country_code => maps:get(geoip_country_code, D, undefined) }. -spec extract_extra_fields(map(), map() | undefined) -> map(). diff --git a/fluxer_gateway/src/session/session_lifecycle.erl b/fluxer_gateway/src/session/session_lifecycle.erl index cd769d2ce..6dcc4f9a1 100644 --- a/fluxer_gateway/src/session/session_lifecycle.erl +++ b/fluxer_gateway/src/session/session_lifecycle.erl @@ -604,6 +604,7 @@ serialize_state(State) -> shard => maps:get(shard, State, undefined), e2ee_capable => maps:get(e2ee_capable, State, false), thread_channels_capable => maps:get(thread_channels_capable, State, false), + geoip_country_code => maps:get(geoip_country_code, State, undefined), guilds => maps:get(guilds, State, #{}), active_guilds => maps:get(active_guilds, State, sets:new()), collected_guild_states => maps:get(collected_guild_states, State), @@ -638,6 +639,7 @@ serialize_transfer_identity(State) -> bot => maps:get(bot, State, false), shard => maps:get(shard, State, undefined), e2ee_capable => maps:get(e2ee_capable, State, false), + geoip_country_code => maps:get(geoip_country_code, State, undefined), ignored_events => maps:keys(maps:get(ignored_events, State, #{})), initial_guild_id => maps:get(initial_guild_id, State, undefined), active_guilds => maps:get(active_guilds, State, sets:new()), diff --git a/fluxer_gateway/src/session/session_manager_shard_drain.erl b/fluxer_gateway/src/session/session_manager_shard_drain.erl index 3984b6b15..1199334f2 100644 --- a/fluxer_gateway/src/session/session_manager_shard_drain.erl +++ b/fluxer_gateway/src/session/session_manager_shard_drain.erl @@ -199,6 +199,7 @@ build_session_data(Data, IdentifyData, Version, SocketPid, SessionId, UserDataMa ), bot => map_utils:get_safe(UserDataMap, <<"bot">>, false), e2ee_capable => extract_e2ee_capable(Properties), + geoip_country_code => extract_geoip_country_code(Data), ignored_events => term_detach:detach(IgnoredEvents), initial_guild_id => map_utils:get_safe(IdentifyData, initial_guild_id, undefined), shard => Shard, @@ -386,6 +387,13 @@ extract_e2ee_capable(Properties) when is_map(Properties) -> extract_e2ee_capable(_) -> false. +-spec extract_geoip_country_code(map()) -> binary() | undefined. +extract_geoip_country_code(Data) -> + case maps:get(<<"geoip_country_code">>, Data, null) of + CountryCode when is_binary(CountryCode), byte_size(CountryCode) > 0 -> CountryCode; + _ -> undefined + end. + -spec fetch_rpc_data(map(), term()) -> {ok, map()} | {error, term()}. fetch_rpc_data(Request, PeerIP) -> StartTime = erlang:system_time(millisecond), diff --git a/fluxer_gateway/src/session/session_voice.erl b/fluxer_gateway/src/session/session_voice.erl index d25c32975..ff61c7f3e 100644 --- a/fluxer_gateway/src/session/session_voice.erl +++ b/fluxer_gateway/src/session/session_voice.erl @@ -7,6 +7,8 @@ init_voice_queue/0, process_voice_queue/1, handle_voice_state_update/2, + report_rejection/2, + handle_voice_signal/2, handle_voice_disconnect/1 ]). @@ -37,7 +39,7 @@ process_voice_queue_item(Item, State) -> case maps:get(type, Item, undefined) of voice_state_update -> Data = maps:get(data, Item), - {reply, _, NewState} = session_voice_connect:handle_voice_state_update(Data, State), + {reply, _, NewState} = handle_voice_state_update(Data, State), NewState; _ -> State @@ -45,7 +47,64 @@ process_voice_queue_item(Item, State) -> -spec handle_voice_state_update(map(), session_state()) -> voice_state_reply(). handle_voice_state_update(Data, State) -> - session_voice_connect:handle_voice_state_update(Data, State). + Result = session_voice_connect:handle_voice_state_update(Data, State), + ok = report_rejected_reply(Result, State), + Result. + +-spec report_rejected_reply(voice_state_reply(), session_state()) -> ok. +report_rejected_reply({reply, {error, _Category, ErrorAtom}, _NewState}, State) -> + report_rejection(ErrorAtom, State); +report_rejected_reply(_Result, _State) -> + ok. + +-spec report_rejection(term(), session_state()) -> ok. +report_rejection(ErrorAtom, State) -> + case {voice_p2p:reports_rejection(ErrorAtom), maps:get(socket_pid, State, undefined)} of + {true, SocketPid} when is_pid(SocketPid) -> + SocketPid ! {gateway_error, ErrorAtom}, + ok; + _ -> + ok + end. + +-spec handle_voice_signal(map(), session_state()) -> ok. +handle_voice_signal(Data, State) -> + GuildIdResult = validation:validate_optional_snowflake( + maps:get(<<"guild_id">>, Data, null) + ), + ChannelIdResult = validation:validate_snowflake(maps:get(<<"channel_id">>, Data, null)), + To = maps:get(<<"to">>, Data, undefined), + Signal = maps:get(<<"data">>, Data, undefined), + case {GuildIdResult, ChannelIdResult} of + {{ok, GuildId}, {ok, ChannelId}} when is_binary(To), is_map(Signal) -> + route_voice_signal(GuildId, ChannelId, To, Signal, State); + _ -> + ok + end. + +-spec route_voice_signal(integer() | null, integer(), binary(), map(), session_state()) -> ok. +route_voice_signal(null, ChannelId, To, Signal, State) -> + case maps:get(ChannelId, maps:get(calls, State, #{}), undefined) of + {CallPid, _Ref} when is_pid(CallPid) -> + gen_server:cast(CallPid, {voice_signal, maps:get(id, State), To, Signal}); + _ -> + ok + end; +route_voice_signal(GuildId, ChannelId, To, Signal, State) -> + case maps:get(GuildId, maps:get(guilds, State, #{}), undefined) of + {GuildPid, _Ref} when is_pid(GuildPid) -> + gen_server:cast( + GuildPid, + {voice_signal, #{ + session_id => maps:get(id, State), + channel_id => ChannelId, + to => To, + data => Signal + }} + ); + _ -> + ok + end. -spec handle_voice_disconnect(session_state()) -> voice_state_reply(). handle_voice_disconnect(State) -> @@ -88,6 +147,82 @@ init_voice_queue_test() -> ?assert(queue:is_empty(maps:get(voice_queue, Result))), ok. +report_rejection_sends_p2p_rejections_to_the_socket_test() -> + State = #{socket_pid => self()}, + ok = report_rejection(voice_p2p_consent_required, State), + ok = report_rejection(voice_p2p_unavailable, State), + ok = report_rejection(voice_channel_full, State), + ok = report_rejection(voice_token_failed, State), + ok = report_rejection(voice_channel_full, #{socket_pid => undefined}), + ?assertEqual( + [voice_p2p_consent_required, voice_p2p_unavailable, voice_channel_full], + drain_gateway_errors() + ). + +drain_gateway_errors() -> + receive + {gateway_error, ErrorAtom} -> [ErrorAtom | drain_gateway_errors()] + after 50 -> + [] + end. + +voice_signal_routes_to_the_guild_test() -> + State = #{id => <<"sess">>, guilds => #{42 => {self(), make_ref()}}, calls => #{}}, + Data = #{ + <<"guild_id">> => <<"42">>, + <<"channel_id">> => <<"100">>, + <<"to">> => <<"conn-b">>, + <<"data">> => #{<<"type">> => <<"offer">>} + }, + ok = handle_voice_signal(Data, State), + receive + {'$gen_cast', {voice_signal, Request}} -> + ?assertEqual( + #{ + session_id => <<"sess">>, + channel_id => 100, + to => <<"conn-b">>, + data => #{<<"type">> => <<"offer">>} + }, + Request + ) + after 1000 -> + error(voice_signal_not_routed) + end. + +voice_signal_routes_to_the_call_test() -> + State = #{id => <<"sess">>, guilds => #{}, calls => #{100 => {self(), make_ref()}}}, + Data = #{<<"channel_id">> => <<"100">>, <<"to">> => <<"conn-b">>, <<"data">> => #{}}, + ok = handle_voice_signal(Data, State), + receive + {'$gen_cast', {voice_signal, <<"sess">>, <<"conn-b">>, #{}}} -> ok + after 1000 -> + error(voice_signal_not_routed) + end. + +voice_signal_drops_unroutable_frames_test() -> + State = #{id => <<"sess">>, guilds => #{42 => {self(), make_ref()}}, calls => #{}}, + Valid = #{ + <<"guild_id">> => <<"42">>, + <<"channel_id">> => <<"100">>, + <<"to">> => <<"conn-b">>, + <<"data">> => #{} + }, + Dropped = [ + Valid#{<<"guild_id">> => <<"43">>}, + Valid#{<<"guild_id">> => null}, + Valid#{<<"channel_id">> => null}, + Valid#{<<"to">> => 5}, + Valid#{<<"data">> => <<"sdp">>}, + #{} + ], + [?assertEqual(ok, handle_voice_signal(Data, State)) || Data <- Dropped], + receive + {'$gen_cast', Unexpected} -> error({unexpected_voice_signal, Unexpected}) + after 50 -> + ok + end. + process_voice_queue_empty_test() -> State = #{voice_queue => queue:new()}, Result = process_voice_queue(State), diff --git a/fluxer_gateway/src/session/session_voice_connect.erl b/fluxer_gateway/src/session/session_voice_connect.erl index a1b77beb2..748221270 100644 --- a/fluxer_gateway/src/session/session_voice_connect.erl +++ b/fluxer_gateway/src/session/session_voice_connect.erl @@ -47,6 +47,7 @@ handle_voice_state_update(Data, State) -> user_id => UserId, e2ee_capable => E2EECapable, bot => Bot, + country_code => maps:get(geoip_country_code, State, undefined), guilds => Guilds }, dispatch_validated(GuildIdResult, ChannelIdResult, Ctx, State). @@ -64,7 +65,8 @@ extract_voice_params(Data) -> viewer_stream_keys => maps:get(<<"viewer_stream_keys">>, Data, undefined), is_mobile => maps:get(<<"is_mobile">>, Data, false), latitude => maps:get(<<"latitude">>, Data, null), - longitude => maps:get(<<"longitude">>, Data, null) + longitude => maps:get(<<"longitude">>, Data, null), + p2p => maps:get(<<"p2p">>, Data, undefined) }. -spec dispatch_validated( @@ -148,13 +150,14 @@ handle_dm_channel(ChId, Ctx, State) -> user_id := UserId, params := Params, e2ee_capable := E2EE, - bot := Bot + bot := Bot, + country_code := CountryCode } = Ctx, ConnId = maps:get(connection_id, Params), case is_binary(ConnId) orelse ConnId =:= null of true -> handle_dm_connect( - ChId, Params, SId, UserId, E2EE, Bot, State + ChId, Params, SId, UserId, E2EE, Bot, CountryCode, State ); false -> invalid_params_reply(UserId, SId, State) @@ -257,9 +260,10 @@ log_dm_disconnect_err(UserId, SId, ConnId, Cat, Err) -> user_id(), boolean(), boolean(), + binary() | undefined, session_state() ) -> voice_state_reply(). -handle_dm_connect(ChId, Params, SId, UserId, E2EE, Bot, State) -> +handle_dm_connect(ChId, Params, SId, UserId, E2EE, Bot, CountryCode, State) -> ConnId = maps:get(connection_id, Params), Request = #{ user_id => UserId, @@ -275,7 +279,9 @@ handle_dm_connect(ChId, Params, SId, UserId, E2EE, Bot, State) -> latitude => maps:get(latitude, Params), longitude => maps:get(longitude, Params), e2ee_capable => E2EE, - bot => Bot + bot => Bot, + p2p => maps:get(p2p, Params), + country_code => CountryCode }, StWithPid = State#{session_pid => self()}, Result = dm_voice:voice_state_update(Request, StWithPid), @@ -403,11 +409,12 @@ guild_voice_queue(GuildPid, GId, ChId, Ctx, State) -> user_id := UserId, params := Params, e2ee_capable := E2EE, - bot := Bot + bot := Bot, + country_code := CountryCode } = Ctx, ConnId = maps:get(connection_id, Params), log_guild_info("guild_queue", UserId, SId, GId, ChId, ConnId), - Req = build_guild_request(ChId, Params, UserId, SId, E2EE, Bot), + Req = build_guild_request(ChId, Params, UserId, SId, E2EE, Bot, CountryCode), VoiceCtx = #{ guild_pid => GuildPid, guild_id => GId, @@ -485,9 +492,10 @@ log_guild_warning(Tag, UserId, SId, GId, ChId, ConnId) -> user_id(), binary(), boolean(), - boolean() + boolean(), + binary() | undefined ) -> map(). -build_guild_request(ChId, Params, UserId, SId, E2EE, Bot) -> +build_guild_request(ChId, Params, UserId, SId, E2EE, Bot, CountryCode) -> #{ user_id => UserId, session_id => SId, @@ -502,5 +510,7 @@ build_guild_request(ChId, Params, UserId, SId, E2EE, Bot) -> latitude => maps:get(latitude, Params), longitude => maps:get(longitude, Params), e2ee_capable => E2EE, - bot => Bot + bot => Bot, + p2p => maps:get(p2p, Params), + country_code => CountryCode }. diff --git a/fluxer_gateway/src/session/session_voice_dispatch.erl b/fluxer_gateway/src/session/session_voice_dispatch.erl index 90a267c8e..d1dbaa9de 100644 --- a/fluxer_gateway/src/session/session_voice_dispatch.erl +++ b/fluxer_gateway/src/session/session_voice_dispatch.erl @@ -76,7 +76,8 @@ handle_guild_call_result({error, timeout}, Ctx, _SessionPid) -> log_voice_warning("voice_state_update_guild_call_timeout", Ctx); handle_guild_call_result({error, noproc}, Ctx, _SessionPid) -> log_voice_warning("voice_state_update_guild_call_noproc", Ctx); -handle_guild_call_result({error, Cat, Err}, Ctx, _SessionPid) -> +handle_guild_call_result({error, Cat, Err}, Ctx, SessionPid) -> + gen_server:cast(SessionPid, {voice_rejected, Err}), log_voice_warning_extra(Ctx, " category=~p error=~p", [Cat, Err]); handle_guild_call_result({error, Reason}, Ctx, _SessionPid) -> log_voice_warning_extra(Ctx, " reason=~p", [Reason]). @@ -107,6 +108,15 @@ handle_guild_reply_ok(Reply, Ctx, SessionPid) -> -spec maybe_dispatch_voice_server_update( map(), guild_id(), channel_id() | null, pid() ) -> ok. +maybe_dispatch_voice_server_update( + #{p2p := true, ice_servers := IceServers, connection_id := ConnId}, GId, ChId, SessionPid +) when is_integer(ChId) -> + dispatch_to_session( + SessionPid, + voice_server_update, + voice_p2p:voice_server_update(ConnId, ChId, GId, IceServers), + GId + ); maybe_dispatch_voice_server_update(Reply, GId, ChId, SessionPid) -> Token = maps:get(token, Reply, undefined), Endpoint = maps:get(endpoint, Reply, undefined), @@ -371,6 +381,37 @@ join_reply_dispatches_voice_server_update_test() -> ?assertEqual(<<"conn-1">>, maps:get(<<"connection_id">>, Payload)), ?assertNot(maps:is_key(<<"e2ee_key">>, Payload)). +guild_rejection_is_cast_to_the_session_test() -> + ok = handle_guild_call_result( + {error, permission_denied, voice_p2p_consent_required}, test_voice_ctx(456), self() + ), + receive + {'$gen_cast', {voice_rejected, voice_p2p_consent_required}} -> ok + after 1000 -> + error(voice_rejection_not_cast) + end. + +p2p_join_reply_dispatches_ice_servers_without_a_token_test() -> + IceServers = [#{<<"urls">> => [<<"stun:stun.example:3478">>]}], + Reply = #{ + success => true, + p2p => true, + ice_servers => IceServers, + connection_id => <<"conn-1">>, + voice_state => #{<<"channel_id">> => <<"456">>} + }, + ok = handle_guild_reply_ok(Reply, test_voice_ctx(456), self()), + ?assertEqual( + #{ + <<"p2p">> => true, + <<"ice_servers">> => IceServers, + <<"guild_id">> => <<"42">>, + <<"channel_id">> => <<"456">>, + <<"connection_id">> => <<"conn-1">> + }, + receive_dispatch(voice_server_update) + ). + join_reply_attaches_e2ee_key_test() -> Reply = #{ success => true, diff --git a/fluxer_gateway/src/utils/constants.erl b/fluxer_gateway/src/utils/constants.erl index 74f9a0ceb..fef1d1e16 100644 --- a/fluxer_gateway/src/utils/constants.erl +++ b/fluxer_gateway/src/utils/constants.erl @@ -47,6 +47,7 @@ gateway_opcode(9) -> invalid_session; gateway_opcode(10) -> hello; gateway_opcode(11) -> heartbeat_ack; gateway_opcode(12) -> gateway_error; +gateway_opcode(13) -> voice_signal; gateway_opcode(14) -> lazy_request; gateway_opcode(15) -> request_guild_counts; gateway_opcode(16) -> request_channel_member_counts; @@ -67,6 +68,7 @@ opcode_to_num(invalid_session) -> 9; opcode_to_num(hello) -> 10; opcode_to_num(heartbeat_ack) -> 11; opcode_to_num(gateway_error) -> 12; +opcode_to_num(voice_signal) -> 13; opcode_to_num(lazy_request) -> 14; opcode_to_num(request_guild_counts) -> 15; opcode_to_num(request_channel_member_counts) -> 16; @@ -108,7 +110,7 @@ status_type_atom(invisible) -> <<"invisible">>; status_type_atom(offline) -> <<"offline">>. -spec max_payload_size() -> pos_integer(). -max_payload_size() -> 4096. +max_payload_size() -> 16384. -spec heartbeat_interval() -> pos_integer(). heartbeat_interval() -> 41250. @@ -210,7 +212,7 @@ status_type_atom_atom_to_binary_test() -> ?assertEqual(<<"idle">>, status_type_atom(idle)). constants_values_test() -> - ?assertEqual(4096, max_payload_size()), + ?assertEqual(16384, max_payload_size()), ?assertEqual(41250, heartbeat_interval()), ?assertEqual(45000, heartbeat_timeout()), ?assertEqual(60000, resume_timeout()), diff --git a/fluxer_gateway/test/call_p2p_tests.erl b/fluxer_gateway/test/call_p2p_tests.erl new file mode 100644 index 000000000..dae953dca --- /dev/null +++ b/fluxer_gateway/test/call_p2p_tests.erl @@ -0,0 +1,697 @@ +%% SPDX-License-Identifier: AGPL-3.0-or-later + +-module(call_p2p_tests). +-typing([eqwalizer]). + +-include_lib("eunit/include/eunit.hrl"). + +agreed_joiners_form_a_mesh_without_pending_connections_test() -> + with_stubs(fun() -> + State = join_all([{1, true}, {2, true}], call_state()), + ?assertEqual([true, true], p2p_flags(State)), + ?assertEqual(#{}, maps:get(pending_connections, State)), + ?assertEqual([], drain_voice_server_updates()) + end). + +sfu_join_keeps_its_pending_connection_test() -> + with_stubs(fun() -> + State = join_all([{1, false}], call_state()), + ?assert(maps:is_key(connection_id(1), maps:get(pending_connections, State))) + end). + +race_sfu_join_after_p2p_start_is_rejected_test() -> + with_stubs(fun() -> + State = join_all([{1, true}], call_state()), + ?assertEqual( + {reply, {error, voice_p2p_consent_required}, State}, join(2, false, State) + ), + ?assertEqual([], drain_voice_server_updates()) + end). + +race_p2p_join_after_sfu_start_is_rejected_test() -> + with_stubs(fun() -> + State = join_all([{2, false}], call_state()), + ?assertEqual( + {reply, {error, voice_p2p_consent_required}, State}, join(1, true, State) + ), + ?assertEqual([], drain_voice_server_updates()) + end). + +race_async_join_producing_mixed_states_is_rejected_test() -> + with_stubs(fun() -> + State = join_all([{1, true}], call_state()), + ?assertEqual( + {noreply, State}, + call_voice:handle_join_async( + 2, voice_state(2, false), session_id(2), erlang:get(call_p2p_sink), State + ) + ), + receive + {call_join_result, 1, Result} -> + ?assertEqual({error, voice_p2p_consent_required}, Result) + after 1000 -> + error(call_join_result_not_sent) + end, + ?assertEqual([], drain_voice_server_updates()) + end). + +fifth_joiner_over_the_cap_is_rejected_test() -> + with_stubs(fun() -> + Mesh = join_all([{UserId, true} || UserId <- [1, 2, 3, 4]], call_state()), + ?assertEqual([true, true, true, true], p2p_flags(Mesh)), + ?assertEqual({reply, {error, voice_channel_full}, Mesh}, join(5, true, Mesh)), + ?assertEqual([], drain_voice_server_updates()) + end). + +race_join_past_the_configured_cap_is_rejected_test() -> + with_stubs(fun() -> + Mesh = join_all([{1, true}, {2, true}], call_state()), + {reply, ok, Full} = call:handle_call(capped_join(3, 3), from(), Mesh), + ?assertEqual([true, true, true], p2p_flags(Full)), + ?assertEqual( + {reply, {error, voice_channel_full}, Full}, + call:handle_call(capped_join(4, 3), from(), Full) + ), + ?assertNot(maps:is_key(4, maps:get(voice_states, Full))), + ?assertEqual(#{}, maps:get(pending_connections, Full)), + ?assertEqual([], drain_voice_server_updates()) + end). + +join_without_a_cap_falls_back_to_the_hard_ceiling_test() -> + with_stubs(fun() -> + Mesh = join_all([{UserId, true} || UserId <- [1, 2, 3]], call_state()), + {reply, ok, Full} = call:handle_call( + {join, 4, voice_state(4, true), session_id(4), erlang:get(call_p2p_sink), + connection_id(4)}, + from(), + Mesh + ), + ?assertEqual([true, true, true, true], p2p_flags(Full)) + end). + +session_p2p_grant_passes_the_cap_to_the_call_join_test() -> + with_stubs(fun() -> + meck:new(gateway_rpc_call_lookup, [passthrough, no_link]), + try + {reply, #{success := true}, _State} = dm_join(true, [voice_state(20, true)]), + ?assertMatch( + [{join, 10, #{<<"p2p">> := true}, <<"sess">>, _Pid, _ConnectionId, 3}], + [ + Request + || {_Caller, {gateway_rpc_call_lookup, safe_gen_server_call, [_, Request, _]}, + _Result} <- meck:history(gateway_rpc_call_lookup), + element(1, Request) =:= join + ] + ) + after + meck:unload(gateway_rpc_call_lookup) + end + end). + +rejected_join_rolls_back_without_retrying_test() -> + with_stubs(fun() -> + erlang:put(call_p2p_join_reply, {error, voice_p2p_consent_required}), + ok = dm_voice_token:join_or_create_call( + 100, 10, voice_state(10, false), <<"sess">>, self(), undefined + ), + ?assertEqual( + [ + {voice_rejected, voice_p2p_consent_required}, + {call_force_disconnect, 100, connection_id(10)} + ], + drain_session_casts() + ), + ?assertEqual(1, meck:num_calls(call_manager, lookup, '_')) + end). + +explicit_p2p_false_update_converts_test() -> + with_stubs(fun() -> + State0 = join_all([{1, true}, {2, true}], call_state()), + {reply, ok, State1} = call:handle_call( + {update_voice_state, 1, voice_state(1, false)}, from(), State0 + ), + ?assertEqual([false, false], p2p_flags(State1)), + ?assertMatch(#{<<"p2p">> := false}, receive_voice_state_update(connection_id(2))), + ?assertEqual( + [connection_id(1), connection_id(2)], lists:sort(drain_voice_server_updates()) + ) + end). + +update_keeping_p2p_keeps_the_mesh_test() -> + with_stubs(fun() -> + State0 = join_all([{1, true}, {2, true}], call_state()), + {reply, ok, State1} = call:handle_call( + {update_voice_state, 1, (voice_state(1, true))#{<<"self_mute">> => true}}, + from(), + State0 + ), + ?assertEqual([true, true], p2p_flags(State1)), + ?assertEqual([], drain_voice_server_updates()) + end). + +stale_p2p_update_after_conversion_stays_sfu_test() -> + with_stubs(fun() -> + Mesh = join_all([{1, true}, {2, true}], call_state()), + {reply, ok, State0} = call:handle_call( + {update_voice_state, 2, voice_state(2, false)}, from(), Mesh + ), + ?assertEqual(2, length(drain_voice_server_updates())), + ok = flush_user_dispatches(), + {reply, ok, State1} = call:handle_call( + {update_voice_state, 1, voice_state(1, true)}, from(), State0 + ), + ?assertEqual([false, false], p2p_flags(State1)), + ?assertMatch(#{<<"p2p">> := false}, receive_voice_state_update(connection_id(1))), + ?assertEqual([], drain_voice_server_updates()) + end). + +same_region_update_converts_a_mesh_test() -> + with_stubs(fun() -> + State0 = join_all([{1, true}, {2, true}], (call_state())#{region => <<"us-east">>}), + ok = flush_user_dispatches(), + {reply, ok, State1} = call:handle_call({update_region, <<"us-east">>}, from(), State0), + ?assertEqual([false, false], p2p_flags(State1)), + ?assertEqual( + [connection_id(1), connection_id(2)], lists:sort(drain_voice_server_updates()) + ), + ?assertEqual( + [<<"us-east">>, <<"us-east">>], + [maps:get(<<"rtc_region">>, Request) || Request <- token_requests()] + ), + ?assertMatch( + #{voice_states := [#{<<"p2p">> := false}, #{<<"p2p">> := false}]}, + receive_user_dispatch(call_update) + ) + end). + +same_region_update_leaves_an_sfu_call_alone_test() -> + with_stubs(fun() -> + State0 = join_all([{1, false}, {2, false}], (call_state())#{region => <<"us-east">>}), + {reply, ok, _State1} = call:handle_call({update_region, <<"us-east">>}, from(), State0), + ?assertEqual([], drain_voice_server_updates()) + end). + +call_event_and_handoff_keep_the_p2p_flag_test() -> + with_stubs(fun() -> + State = join_all([{1, true}], call_state()), + ?assertMatch( + #{voice_states := [#{<<"p2p">> := true}]}, call_state:build_call_event(State) + ), + Restored = call_handoff:restore_state(call_handoff:export_state(State)), + ?assertEqual([true], p2p_flags(Restored)) + end). + +signal_relay_allowed_test() -> + with_stubs(fun() -> + State = join_all([{1, true}, {2, true}], call_state()), + Data = #{<<"type">> => <<"answer">>, <<"sdp">> => <<"v=0">>}, + {noreply, State} = call:handle_cast( + {voice_signal, session_id(1), connection_id(2), Data}, State + ), + ?assertEqual( + #{ + <<"channel_id">> => <<"1">>, + <<"from">> => connection_id(1), + <<"user_id">> => <<"1">>, + <<"data">> => Data + }, + receive_voice_signal() + ) + end). + +signal_relay_denied_test() -> + with_stubs(fun() -> + Mesh = join_all([{1, true}, {2, true}], call_state()), + Data = #{<<"type">> => <<"candidate">>}, + Denied = [ + {voice_signal, session_id(9), connection_id(2), Data}, + {voice_signal, session_id(1), connection_id(1), Data}, + {voice_signal, session_id(1), <<"unknown">>, Data} + ], + [{noreply, Mesh} = call:handle_cast(Signal, Mesh) || Signal <- Denied], + {reply, ok, Sfu} = call:handle_call({update_region, <<"us-east">>}, from(), Mesh), + {noreply, Sfu} = call:handle_cast( + {voice_signal, session_id(1), connection_id(2), Data}, Sfu + ), + assert_no_voice_signal() + end). + +session_p2p_start_without_a_call_test() -> + with_stubs(fun() -> + {reply, #{success := true, connection_id := ConnectionId}, State} = dm_join( + true, not_found + ), + ?assertMatch( + #{ + <<"p2p">> := true, + <<"p2p_initiator">> := true, + <<"p2p_participant_count">> := 1, + <<"country_code">> := <<"SE">> + }, + lists:last(token_requests()) + ), + ?assertEqual( + true, maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, State))) + ), + Update = receive_session_voice_server_update(), + ?assertEqual( + #{ + <<"p2p">> => true, + <<"ice_servers">> => [#{<<"urls">> => [<<"stun:stun.example:3478">>]}], + <<"connection_id">> => ConnectionId, + <<"channel_id">> => <<"100">> + }, + Update + ) + end). + +session_p2p_join_of_an_existing_mesh_test() -> + with_stubs(fun() -> + {reply, #{success := true}, _State} = dm_join(true, [voice_state(20, true)]), + ?assertMatch( + #{ + <<"p2p">> := true, + <<"p2p_initiator">> := false, + <<"p2p_participant_count">> := 2 + }, + lists:last(token_requests()) + ), + ?assertMatch(#{<<"p2p">> := true}, receive_session_voice_server_update()) + end). + +session_join_from_a_second_device_counts_the_user_once_test() -> + with_stubs(fun() -> + {reply, #{success := true}, _State} = dm_join( + true, [voice_state(20, true), voice_state(10, true)] + ), + ?assertMatch( + #{ + <<"p2p">> := true, + <<"p2p_initiator">> := false, + <<"p2p_participant_count">> := 2 + }, + lists:last(token_requests()) + ) + end). + +session_join_of_an_sfu_call_does_not_ask_for_p2p_test() -> + with_stubs(fun() -> + {reply, #{success := true, connection_id := ConnectionId}, State} = dm_join( + true, [voice_state(20, false)] + ), + ?assertNot(maps:is_key(<<"p2p">>, lists:last(token_requests()))), + ?assertEqual( + false, maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, State))) + ), + Update = receive_session_voice_server_update(), + ?assertEqual(<<"tok">>, maps:get(<<"token">>, Update)), + ?assertNot(maps:is_key(<<"p2p">>, Update)) + end). + +session_join_of_a_mesh_without_agreement_is_rejected_test() -> + with_stubs(fun() -> + ?assertMatch( + {reply, {error, permission_denied, voice_p2p_consent_required}, #{ + dm_voice_states := #{} + }}, + dm_join_reply(undefined, [voice_state(20, true)]) + ), + ?assertEqual([], token_requests()), + assert_no_session_voice_server_update() + end). + +session_bot_join_of_a_mesh_is_rejected_test() -> + with_stubs(fun() -> + erlang:put(call_p2p_call_voice_states, [voice_state(20, true)]), + State = dm_session_state(#{}), + ?assertMatch( + {reply, {error, permission_denied, voice_p2p_consent_required}, _State}, + dm_voice_token:get_dm_voice_token_and_create_state( + (dm_request(true, State))#{bot => true} + ) + ) + end). + +session_join_of_a_full_mesh_is_rejected_test() -> + with_stubs(fun() -> + Mesh = [voice_state(UserId, true) || UserId <- [20, 21, 22, 23]], + ?assertMatch( + {reply, {error, permission_denied, voice_channel_full}, _State}, + dm_join_reply(true, Mesh) + ), + ?assertEqual([], token_requests()) + end). + +session_join_past_the_configured_cap_is_rejected_as_full_test() -> + with_stubs(fun() -> + erlang:put(call_p2p_stub_mode, channel_full), + ?assertMatch( + {reply, {error, permission_denied, voice_channel_full}, #{dm_voice_states := #{}}}, + dm_join_reply(true, [voice_state(20, true), voice_state(21, true)]) + ), + ?assertMatch( + #{ + <<"p2p">> := true, + <<"p2p_initiator">> := false, + <<"p2p_participant_count">> := 3 + }, + lists:last(token_requests()) + ), + assert_no_session_voice_server_update() + end). + +session_declined_grant_into_a_mesh_is_rejected_test() -> + with_stubs(fun() -> + erlang:put(call_p2p_stub_mode, decline), + ?assertMatch( + {reply, {error, voice_error, voice_p2p_unavailable}, #{dm_voice_states := #{}}}, + dm_join_reply(true, [voice_state(20, true)]) + ), + ?assertMatch( + #{<<"p2p">> := true, <<"p2p_initiator">> := false}, lists:last(token_requests()) + ), + assert_no_session_voice_server_update() + end). + +session_declined_grant_without_a_call_joins_sfu_test() -> + with_stubs(fun() -> + erlang:put(call_p2p_stub_mode, decline), + {reply, #{success := true, connection_id := ConnectionId}, State} = dm_join( + true, not_found + ), + ?assertEqual( + false, maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, State))) + ), + ?assertEqual(<<"tok">>, maps:get(<<"token">>, receive_session_voice_server_update())) + end). + +session_update_with_explicit_p2p_false_clears_the_flag_test() -> + with_stubs(fun() -> + ?assertEqual(false, dm_updated_p2p(false)), + ?assertEqual(true, dm_updated_p2p(undefined)), + ?assertEqual(true, dm_updated_p2p(true)) + end). + +dm_updated_p2p(P2p) -> + Existing = (voice_state(10, true))#{ + <<"channel_id">> => <<"100">>, <<"session_id">> => <<"sess">> + }, + ConnectionId = connection_id(10), + State = dm_session_state(#{ConnectionId => Existing}), + {reply, #{success := true}, NewState} = dm_voice_connect:handle_dm_connect_or_update( + (dm_request(P2p, State))#{ + connection_id => ConnectionId, viewer_stream_keys => undefined + } + ), + maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, NewState))). + +dm_join_reply(P2p, CallVoiceStates) -> + erlang:put(call_p2p_call_voice_states, CallVoiceStates), + State = dm_session_state(#{}), + dm_voice_token:get_dm_voice_token_and_create_state(dm_request(P2p, State)). + +dm_join(P2p, CallVoiceStates) -> + Reply = dm_join_reply(P2p, CallVoiceStates), + receive + {'$gen_cast', {call_monitor, 100, _CallPid}} -> Reply + after 1000 -> + error(call_not_joined) + end. + +dm_session_state(VoiceStates) -> + #{ + dm_voice_states => VoiceStates, + channels => #{100 => #{<<"type">> => 1, <<"recipient_ids">> => [20]}}, + user_id => 10, + id => <<"sess">>, + session_pid => erlang:get(call_p2p_sink) + }. + +dm_request(P2p, State) -> + #{ + user_id => 10, + channel_id => 100, + session_id => <<"sess">>, + connection_id => null, + self_mute => false, + self_deaf => false, + self_video => false, + self_stream => false, + viewer_stream_keys => [], + is_mobile => false, + latitude => null, + longitude => null, + e2ee_capable => false, + bot => false, + p2p => P2p, + country_code => <<"SE">>, + voice_states => maps:get(dm_voice_states, State), + state => State + }. + +join_all(Joins, State0) -> + lists:foldl( + fun({UserId, P2p}, State) -> + {reply, ok, NewState} = join(UserId, P2p, State), + NewState + end, + State0, + Joins + ). + +join(UserId, P2p, State) -> + call_voice:handle_join_internal( + UserId, + voice_state(UserId, P2p), + session_id(UserId), + erlang:get(call_p2p_sink), + connection_id(UserId), + undefined, + State + ). + +capped_join(UserId, MaxParticipants) -> + {join, UserId, voice_state(UserId, true), session_id(UserId), erlang:get(call_p2p_sink), + connection_id(UserId), MaxParticipants}. + +p2p_flags(State) -> + [ + maps:get(<<"p2p">>, VoiceState) + || {_UserId, VoiceState} <- lists:keysort(1, maps:to_list(maps:get(voice_states, State))) + ]. + +voice_state(UserId, P2p) -> + #{ + <<"user_id">> => integer_to_binary(UserId), + <<"channel_id">> => <<"1">>, + <<"connection_id">> => connection_id(UserId), + <<"session_id">> => session_id(UserId), + <<"p2p">> => P2p + }. + +connection_id(UserId) -> + <<"conn-", (integer_to_binary(UserId))/binary>>. + +session_id(UserId) -> + <<"sess-", (integer_to_binary(UserId))/binary>>. + +from() -> + {self(), make_ref()}. + +call_state() -> + #{ + channel_id => 1, + message_id => 1, + region => undefined, + ringing => [], + pending_ringing => [], + recipients => [1], + voice_states => #{}, + sessions => #{}, + pending_connections => #{}, + initiator_ready => true, + ringing_timers => #{}, + idle_timer => undefined, + created_at => 0, + participants_history => sets:new(), + last_call_event => undefined + }. + +token_requests() -> + [ + Request + || {_Pid, {rpc_client, call, [#{<<"type">> := <<"voice_get_token">>} = Request]}, _Result} <- + meck:history(rpc_client) + ]. + +drain_voice_server_updates() -> + receive + {'$gen_cast', {dispatch, voice_server_update, Payload}} -> + ?assertEqual(<<"tok">>, maps:get(<<"token">>, Payload)), + ?assertNot(maps:is_key(<<"p2p">>, Payload)), + [maps:get(<<"connection_id">>, Payload) | drain_voice_server_updates()] + after 200 -> + [] + end. + +receive_session_voice_server_update() -> + receive + {'$gen_cast', {dispatch, voice_server_update, Payload}} -> Payload + after 1000 -> + error(voice_server_update_not_dispatched) + end. + +assert_no_session_voice_server_update() -> + receive + {'$gen_cast', {dispatch, voice_server_update, Payload}} -> + error({unexpected_voice_server_update, Payload}) + after 100 -> + ok + end. + +drain_session_casts() -> + receive + {'$gen_cast', Cast} -> [Cast | drain_session_casts()] + after 100 -> + [] + end. + +receive_voice_state_update(ConnectionId) -> + case receive_user_dispatch(voice_state_update) of + #{<<"connection_id">> := ConnectionId} = VoiceState -> VoiceState; + _Other -> receive_voice_state_update(ConnectionId) + end. + +receive_user_dispatch(Event) -> + receive + {user_dispatch, Event, Data} -> Data + after 1000 -> + error({user_dispatch_not_received, Event}) + end. + +flush_user_dispatches() -> + receive + {user_dispatch, _Event, _Data} -> flush_user_dispatches() + after 100 -> + ok + end. + +receive_voice_signal() -> + receive + {'$gen_cast', {dispatch, voice_signal, Payload}} -> Payload + after 1000 -> + error(voice_signal_not_dispatched) + end. + +assert_no_voice_signal() -> + receive + {'$gen_cast', {dispatch, voice_signal, Payload}} -> + error({unexpected_voice_signal, Payload}) + after 100 -> + ok + end. + +with_stubs(Fun) -> + Test = self(), + Sink = spawn(fun() -> sink(Test) end), + erlang:put(call_p2p_sink, Sink), + erlang:erase(call_p2p_call_voice_states), + erlang:erase(call_p2p_stub_mode), + erlang:erase(call_p2p_join_reply), + meck:new(rpc_client, [passthrough, no_link, non_strict]), + meck:expect(rpc_client, call, fun rpc_reply/1), + meck:new(presence_manager, [passthrough, no_link]), + meck:expect(presence_manager, dispatch_to_user, fun(_UserId, Event, Data) -> + Sink ! {user_dispatch, Event, Data}, + ok + end), + CallPid = spawn(fun call_stub/0), + meck:new(call_manager, [passthrough, no_link]), + meck:expect(call_manager, lookup, fun(_ChannelId) -> + case erlang:get(call_p2p_call_voice_states) of + not_found -> + {error, not_found}; + VoiceStates when is_list(VoiceStates) -> + CallPid ! {voice_states, VoiceStates}, + {ok, CallPid}; + undefined -> + CallPid ! {join_reply, erlang:get(call_p2p_join_reply)}, + {ok, CallPid} + end + end), + try + Fun() + after + CallPid ! stop, + meck:unload(call_manager), + meck:unload(presence_manager), + meck:unload(rpc_client), + stop_sink(Sink) + end. + +sink(Test) -> + receive + Message -> + Test ! Message, + sink(Test) + after 30000 -> + ok + end. + +stop_sink(Sink) -> + exit(Sink, kill), + flush_mailbox(). + +flush_mailbox() -> + receive + _Message -> flush_mailbox() + after 0 -> + ok + end. + +call_stub() -> + call_stub([], ok). + +call_stub(VoiceStates, JoinReply) -> + receive + {voice_states, NewVoiceStates} -> + call_stub(NewVoiceStates, JoinReply); + {join_reply, undefined} -> + call_stub(VoiceStates, JoinReply); + {join_reply, NewJoinReply} -> + call_stub(VoiceStates, NewJoinReply); + {'$gen_call', From, {get_state}} -> + gen_server:reply(From, {ok, #{voice_states => VoiceStates, region => undefined}}), + call_stub(VoiceStates, JoinReply); + {'$gen_call', From, _Request} -> + gen_server:reply(From, JoinReply), + call_stub(VoiceStates, JoinReply); + stop -> + ok; + _Other -> + call_stub(VoiceStates, JoinReply) + after 30000 -> + ok + end. + +rpc_reply(#{<<"type">> := <<"voice_get_token">>} = Request) -> + ConnectionId = maps:get(<<"connection_id">>, Request, <<"conn-new">>), + case {maps:get(<<"p2p">>, Request, false), erlang:get(call_p2p_stub_mode)} of + {true, channel_full} -> + {ok, #{<<"p2p">> => false, <<"declineReason">> => <<"channel_full">>}}; + {true, Mode} when Mode =/= decline -> + {ok, #{ + <<"p2p">> => true, + <<"connectionId">> => ConnectionId, + <<"iceServers">> => [#{<<"urls">> => [<<"stun:stun.example:3478">>]}], + <<"maxParticipants">> => 3 + }}; + _ -> + {ok, #{ + <<"token">> => <<"tok">>, + <<"endpoint">> => <<"wss://voice.example">>, + <<"connectionId">> => ConnectionId + }} + end; +rpc_reply(_Request) -> + {ok, #{}}. diff --git a/fluxer_gateway/test/call_tests.erl b/fluxer_gateway/test/call_tests.erl index 9aebe6569..d67b6b65e 100644 --- a/fluxer_gateway/test/call_tests.erl +++ b/fluxer_gateway/test/call_tests.erl @@ -86,7 +86,7 @@ join_from_ringing_adds_voice_state_test() -> initiator_ready => true }), {reply, ok, NewState} = call_voice:handle_join_internal( - UserId, VoiceState, SessionId, self(), ConnectionId, State + UserId, VoiceState, SessionId, self(), ConnectionId, undefined, State ), ?assert(maps:is_key(UserId, maps:get(voice_states, NewState))), ?assertNot(lists:member(UserId, maps:get(ringing, NewState))). @@ -165,7 +165,7 @@ leave_then_rejoin_keeps_voice_state_test() -> ) ), {reply, ok, Rejoined} = call_voice:handle_join_internal( - UserId, VoiceState, SessionId, self(), <<"conn-42">>, PostLeave + UserId, VoiceState, SessionId, self(), <<"conn-42">>, undefined, PostLeave ), ?assert(maps:is_key(UserId, maps:get(voice_states, Rejoined))). diff --git a/fluxer_gateway/test/gateway_handler_tests.erl b/fluxer_gateway/test/gateway_handler_tests.erl index 8f3cada30..6dbc0bc1a 100644 --- a/fluxer_gateway/test/gateway_handler_tests.erl +++ b/fluxer_gateway/test/gateway_handler_tests.erl @@ -263,6 +263,26 @@ check_presence_rate_limit_soft_drops_only_presence_test() -> ?assertEqual(1, length(maps:get(events, RateLimitState))) end). +check_voice_signal_rate_limit_allows_trickle_bursts_test() -> + with_rate_limits_enabled(fun() -> + Now = erlang:system_time(millisecond), + State = fun(Count) -> + (gateway_handler:new_state())#{ + rate_limit_state => #{ + events => [], + op_events => #{voice_signal => lists:duplicate(Count, Now - 1000)} + } + } + end, + ?assertMatch( + {ok, _}, gateway_handler_rate_limit:check_rate_limit(State(239), voice_signal) + ), + ?assertMatch( + {opcode_rate_limited, _}, + gateway_handler_rate_limit:check_rate_limit(State(240), voice_signal) + ) + end). + enqueue_voice_update_keeps_latest_update_for_connection_test() -> Queue0 = queue:new(), Data1 = #{ @@ -506,6 +526,47 @@ voice_state_update_with_non_map_payload_closes_test() -> ), ?assertEqual(constants:close_code_to_num(decode_error), CloseCode). +voice_signal_is_cast_to_the_session_test() -> + State = (new_json_state())#{session_pid => self()}, + Data = #{<<"channel_id">> => <<"100">>, <<"to">> => <<"conn-b">>, <<"data">> => #{}}, + ?assertEqual( + {ok, State}, + gateway_handler_dispatch:handle_opcode(voice_signal, #{<<"d">> => Data}, State) + ), + receive + {'$gen_cast', {voice_signal, Data}} -> ok + after 1000 -> + error(voice_signal_not_cast) + end. + +voice_signal_with_non_map_payload_is_dropped_test() -> + State = (new_json_state())#{session_pid => self()}, + ?assertEqual( + {ok, State}, + gateway_handler_dispatch:handle_opcode( + voice_signal, #{<<"d">> => <<"not-a-map">>}, State + ) + ). + +websocket_info_gateway_error_emits_an_error_frame_test() -> + {[{text, Frame}], _State} = gateway_handler:websocket_info( + {gateway_error, voice_p2p_consent_required}, new_json_state() + ), + ?assertEqual( + #{ + <<"op">> => 12, + <<"d">> => #{ + <<"code">> => <<"VOICE_P2P_CONSENT_REQUIRED">>, + <<"message">> => <<"This call is peer-to-peer and requires agreement to join">> + } + }, + json:decode(Frame) + ). + +voice_signal_opcode_is_thirteen_test() -> + ?assertEqual(voice_signal, constants:gateway_opcode(13)), + ?assertEqual(13, constants:opcode_to_num(voice_signal)). + websocket_handle_rejects_compressed_frame_past_max_payload_test() -> {ok, Compressed, _} = gateway_compress:compress( binary:copy(<<"a">>, constants:max_payload_size() * 64), diff --git a/fluxer_gateway/test/guild_voice_broadcast_tests.erl b/fluxer_gateway/test/guild_voice_broadcast_tests.erl index 4a9748473..8ba4feb5a 100644 --- a/fluxer_gateway/test/guild_voice_broadcast_tests.erl +++ b/fluxer_gateway/test/guild_voice_broadcast_tests.erl @@ -46,6 +46,7 @@ broadcast_payload(VoiceState) -> <<"suppress">> => false, <<"viewer_stream_keys">> => [], <<"e2ee_capable">> => false, + <<"p2p">> => false, <<"version">> => 0 }, VoiceState diff --git a/fluxer_gateway/test/guild_voice_p2p_tests.erl b/fluxer_gateway/test/guild_voice_p2p_tests.erl new file mode 100644 index 000000000..9e3312806 --- /dev/null +++ b/fluxer_gateway/test/guild_voice_p2p_tests.erl @@ -0,0 +1,681 @@ +%% SPDX-License-Identifier: AGPL-3.0-or-later + +-module(guild_voice_p2p_tests). +-typing([eqwalizer]). + +-include_lib("eunit/include/eunit.hrl"). + +-define(GUILD_ID, 999). +-define(CHANNEL_ID, 100). +-define(OTHER_CHANNEL_ID, 200). +-define(PINNED_CHANNEL_ID, 300). + +p2p_start_in_an_empty_channel_test() -> + with_stubs(grant, fun(State0) -> + {Reply, State1} = join(10, true, State0), + ConnectionId = maps:get(connection_id, Reply), + ?assertMatch(#{success := true, p2p := true, ice_servers := [_]}, Reply), + ?assertNot(maps:is_key(token, Reply)), + ?assertNot(maps:is_key(endpoint, Reply)), + ?assertEqual(true, p2p_flag(ConnectionId, State1)), + ?assertEqual(#{}, maps:get(pending_voice_connections, State1)), + ?assertMatch( + #{ + <<"p2p">> := true, + <<"p2p_initiator">> := true, + <<"p2p_participant_count">> := 1, + <<"country_code">> := <<"SE">> + }, + last_token_request() + ), + ?assertEqual([{ConnectionId, true}], drain_voice_state_updates()) + end). + +agreed_joiner_joins_the_mesh_test() -> + with_stubs(grant, fun(State0) -> + {ReplyA, State1} = join(10, true, State0), + {ReplyB, State2} = join(11, true, State1), + ?assertMatch(#{p2p := true}, ReplyB), + ?assertMatch( + #{ + <<"p2p">> := true, + <<"p2p_initiator">> := false, + <<"p2p_participant_count">> := 2 + }, + last_token_request() + ), + ?assertEqual(true, p2p_flag(maps:get(connection_id, ReplyA), State2)), + ?assertEqual(true, p2p_flag(maps:get(connection_id, ReplyB), State2)), + ?assertEqual([], served_voice_server_updates(State2)) + end). + +non_agreeing_joiner_is_rejected_and_nobody_is_converted_test() -> + with_stubs(grant, fun(State0) -> + {Mesh, State1} = join_mesh([10, 11], State0), + Requests = token_request_count(), + {Reply, State2} = join(12, undefined, State1), + ?assertEqual({error, permission_denied, voice_p2p_consent_required}, Reply), + ?assertEqual(Requests, token_request_count()), + assert_untouched(Mesh, State1, State2) + end). + +bot_is_rejected_like_a_non_agreeing_joiner_test() -> + with_stubs(grant, fun(State0) -> + {Mesh, State1} = join_mesh([10, 11], State0), + {reply, Reply, State2} = guild_voice_connection:voice_state_update( + (request(12, true))#{bot => true}, State1 + ), + ?assertEqual({error, permission_denied, voice_p2p_consent_required}, Reply), + assert_untouched(Mesh, State1, State2) + end). + +fifth_joiner_is_rejected_as_full_test() -> + with_stubs(grant, fun(State0) -> + {Mesh, State1} = join_mesh([10, 11, 12, 13], State0), + ?assertEqual(4, length(Mesh)), + Requests = token_request_count(), + {Reply, State2} = join(14, true, State1), + ?assertEqual({error, permission_denied, voice_channel_full}, Reply), + ?assertEqual(Requests, token_request_count()), + assert_untouched(Mesh, State1, State2) + end). + +joiner_past_the_configured_cap_is_rejected_as_full_test() -> + with_stubs({cap, 2}, fun(State0) -> + {Mesh, State1} = join_mesh([10, 11], State0), + {Reply, State2} = join(12, true, State1), + ?assertMatch( + #{ + <<"p2p">> := true, + <<"p2p_initiator">> := false, + <<"p2p_participant_count">> := 3 + }, + last_token_request() + ), + ?assertEqual({error, permission_denied, voice_channel_full}, Reply), + assert_untouched(Mesh, State1, State2) + end). + +mesh_fills_up_to_the_configured_cap_test() -> + with_stubs({cap, 3}, fun(State0) -> + {[_, _, _] = Mesh, State1} = join_mesh([10, 11, 12], State0), + [?assertEqual(true, p2p_flag(ConnectionId, State1)) || ConnectionId <- Mesh], + {Reply, _State2} = join(13, true, State1), + ?assertEqual({error, permission_denied, voice_channel_full}, Reply) + end). + +declined_grant_into_a_live_mesh_is_rejected_test() -> + with_stubs(grant, fun(State0) -> + {Mesh, State1} = join_mesh([10, 11], State0), + erlang:put(p2p_stub_mode, decline), + {Reply, State2} = join(12, true, State1), + ?assertMatch(#{<<"p2p">> := true, <<"p2p_initiator">> := false}, last_token_request()), + ?assertEqual({error, voice_error, voice_p2p_unavailable}, Reply), + assert_untouched(Mesh, State1, State2) + end). + +declined_grant_on_an_empty_channel_joins_sfu_test() -> + with_stubs(decline, fun(State0) -> + {Reply, State1} = join(10, true, State0), + ?assertMatch(#{<<"p2p">> := true, <<"p2p_initiator">> := true}, last_token_request()), + ?assertMatch(#{success := true, token := _, endpoint := _}, Reply), + ?assertNot(maps:is_key(p2p, Reply)), + ?assertEqual(#{}, maps:get(voice_states, State1)), + ?assertEqual( + [maps:get(connection_id, Reply)], + maps:keys(maps:get(pending_voice_connections, State1)) + ) + end). + +pinned_empty_channel_joins_a_non_agreeing_joiner_on_sfu_test() -> + with_stubs(grant, fun(State0) -> + {reply, Reply, _State1} = guild_voice_connection:voice_state_update( + (request(10, undefined))#{channel_id => ?PINNED_CHANNEL_ID}, State0 + ), + ?assertMatch(#{success := true, token := _}, Reply), + ?assertNot(maps:is_key(<<"p2p">>, last_token_request())) + end). + +pinned_channel_joins_sfu_when_the_grant_is_declined_test() -> + with_stubs(decline, fun(State0) -> + {reply, Reply, _State1} = guild_voice_connection:voice_state_update( + (request(10, true))#{channel_id => ?PINNED_CHANNEL_ID}, State0 + ), + ?assertMatch(#{success := true, token := _}, Reply) + end). + +pending_join_blocks_a_p2p_start_test() -> + with_stubs(grant, fun(State0) -> + {_Reply, State1} = join(10, undefined, State0), + ?assertEqual(1, maps:size(maps:get(pending_voice_connections, State1))), + {Reply, _State2} = join(11, true, State1), + ?assertMatch(#{token := _}, Reply), + ?assertNot(maps:is_key(<<"p2p">>, last_token_request())) + end). + +explicit_p2p_false_update_converts_test() -> + with_stubs(grant, fun(State0) -> + {[ConnectionA | _] = Mesh, State1} = join_mesh([10, 11], State0), + {reply, #{success := true}, State2} = guild_voice_connection:voice_state_update( + (request(10, false))#{connection_id => ConnectionA}, State1 + ), + assert_converted(Mesh, State2) + end). + +explicit_p2p_false_update_is_ignored_in_a_pinned_channel_test() -> + with_stubs(grant, fun(State0) -> + {[ConnectionA | _] = Mesh, State1} = join_mesh([10, 11], ?PINNED_CHANNEL_ID, State0), + _ = drain_voice_state_updates(), + {reply, #{success := true}, State2} = guild_voice_connection:voice_state_update( + (request(10, false))#{ + channel_id => ?PINNED_CHANNEL_ID, connection_id => ConnectionA + }, + State1 + ), + assert_untouched(Mesh, State1, State2) + end). + +update_without_p2p_keeps_the_mesh_test() -> + with_stubs(grant, fun(State0) -> + {[ConnectionA, ConnectionB], State1} = join_mesh([10, 11], State0), + {reply, #{success := true}, State2} = guild_voice_connection:voice_state_update( + (request(10, undefined))#{connection_id => ConnectionA, self_mute => true}, + State1 + ), + ?assertEqual(true, p2p_flag(ConnectionA, State2)), + ?assertEqual(true, p2p_flag(ConnectionB, State2)), + ?assertEqual( + true, + maps:get(<<"self_mute">>, maps:get(ConnectionA, maps:get(voice_states, State2))) + ) + end). + +region_switch_converts_test() -> + with_stubs(grant, fun(State0) -> + {Mesh, State1} = join_mesh([10, 11], State0), + {reply, #{success := true}, State2} = guild_voice_region:switch_voice_region_handler( + #{channel_id => ?CHANNEL_ID}, State1 + ), + assert_marked_sfu(Mesh, State2), + GuildPid = maps:get(guild_pid, State2), + GuildPid ! {serve, State2}, + ok = guild_voice_region:switch_voice_region(?GUILD_ID, ?CHANNEL_ID, GuildPid), + ?assertEqual(lists:sort(Mesh), lists:sort(drain_voice_server_updates(length(Mesh)))) + end). + +region_switch_keeps_a_pinned_channel_p2p_test() -> + with_stubs(grant, fun(State0) -> + {Mesh, State1} = join_mesh([10, 11], ?PINNED_CHANNEL_ID, State0), + _ = drain_voice_state_updates(), + {reply, #{success := true}, State2} = guild_voice_region:switch_voice_region_handler( + #{channel_id => ?PINNED_CHANNEL_ID}, State1 + ), + GuildPid = maps:get(guild_pid, State2), + GuildPid ! {serve, State2}, + ok = guild_voice_region:switch_voice_region(?GUILD_ID, ?PINNED_CHANNEL_ID, GuildPid), + assert_untouched(Mesh, State1, State2) + end). + +livekit_restore_into_a_mesh_is_refused_test() -> + with_stubs(grant, fun(State0) -> + {Mesh, State1} = join_mesh([10, 11], State0), + Stale = (maps:get(hd(Mesh), maps:get(voice_states, State1)))#{ + <<"connection_id">> => <<"stale">>, <<"user_id">> => <<"12">>, <<"p2p">> => false + }, + State2 = State1#{ + recently_disconnected_voice_states => #{ + <<"stale">> => #{ + voice_state => Stale, disconnected_at => erlang:system_time(millisecond) + } + } + }, + {reply, Reply, State3} = + guild_voice_connection:confirm_voice_connection_from_livekit( + #{connection_id => <<"stale">>}, State2 + ), + ?assertEqual({error, permission_denied, voice_p2p_consent_required}, Reply), + ?assertNot(maps:is_key(<<"stale">>, maps:get(voice_states, State3))), + assert_untouched(Mesh, State2, State3) + end). + +livekit_activation_into_a_mesh_is_refused_test() -> + with_stubs(grant, fun(State0) -> + {Mesh, State1} = join_mesh([10, 11], State0), + State2 = guild_voice_connection_pending:store_pending( + <<"late">>, + #{ + user_id => 12, + guild_id => ?GUILD_ID, + channel_id => ?CHANNEL_ID, + session_id => session_id(12), + token_nonce => <<"nonce">>, + expires_at => erlang:system_time(millisecond) + 30000 + }, + State1 + ), + {reply, Reply, State3} = + guild_voice_connection:confirm_voice_connection_from_livekit( + #{connection_id => <<"late">>, token_nonce => <<"nonce">>}, State2 + ), + ?assertEqual({error, permission_denied, voice_p2p_consent_required}, Reply), + ?assertEqual(lists:sort(Mesh), lists:sort(maps:keys(maps:get(voice_states, State3)))), + ?assertEqual(#{}, maps:get(pending_voice_connections, State3)), + assert_untouched(Mesh, State1, State3) + end). + +explicit_switch_converts_without_a_voice_server_test() -> + with_stubs(grant, fun(State0) -> + {[ConnectionA | _] = Mesh, State1} = join_mesh([10, 11], State0), + Self = self(), + Guild = spawn(fun() -> + {reply, Reply, State2} = guild_voice_connection:voice_state_update( + (request(10, false))#{connection_id => ConnectionA}, + maps:remove(guild_pid, State1) + ), + Self ! {converted, Reply, State2}, + guild_stub_loop(State2) + end), + receive + {converted, Reply, State2} -> + ?assertMatch(#{success := true}, Reply), + assert_marked_sfu(Mesh, State2), + ?assertEqual(lists:sort(Mesh), lists:sort(drain_voice_server_updates(2))) + after 2000 -> + error(explicit_switch_did_not_convert) + end, + Guild ! stop + end). + +moderator_move_into_a_mesh_is_refused_test() -> + with_stubs(grant, fun(State0) -> + {Mesh, State1} = join_mesh([10, 11], State0), + State2 = with_sfu_voice_state(<<"moved">>, 12, ?OTHER_CHANNEL_ID, State1), + {reply, Reply, State3} = guild_voice:move_member( + move_request(12, ?CHANNEL_ID), State2 + ), + ?assertEqual({error, permission_denied, voice_p2p_consent_required}, Reply), + ?assert(maps:is_key(<<"moved">>, maps:get(voice_states, State3))), + assert_untouched(Mesh, State2, State3) + end). + +moderator_move_into_an_empty_pinned_channel_moves_on_sfu_test() -> + with_stubs(grant, fun(State0) -> + State1 = with_sfu_voice_state(<<"moved">>, 12, ?OTHER_CHANNEL_ID, State0), + {reply, Reply, _State2} = guild_voice:move_member( + move_request(12, ?PINNED_CHANNEL_ID), State1 + ), + ?assertMatch(#{success := true, needs_token := true}, Reply) + end). + +moderator_move_out_of_a_mesh_gets_an_sfu_grant_test() -> + with_stubs(grant, fun(State0) -> + {[ConnectionA, ConnectionB], State1} = join_mesh([10, 11], State0), + {reply, #{success := true, needs_token := true, session_data := SessionData}, State2} = + guild_voice:move_member(move_request(11, ?OTHER_CHANNEL_ID), State1), + ?assertEqual([ConnectionA], maps:keys(maps:get(voice_states, State2))), + ?assertEqual(true, p2p_flag(ConnectionA, State2)), + GuildPid = maps:get(guild_pid, State2), + GuildPid ! {serve, State2}, + ok = guild_voice_move_execute:send_single_voice_server_update( + ?GUILD_ID, ?OTHER_CHANNEL_ID, hd(SessionData), GuildPid + ), + ?assertEqual([ConnectionB], drain_voice_server_updates(1)), + ?assertNot(maps:is_key(<<"p2p">>, last_token_request())) + end). + +client_move_into_a_mesh_joins_it_test() -> + with_stubs(grant, fun(State0) -> + {[ConnectionA], State1} = join_mesh([10], State0), + State2 = with_sfu_voice_state(<<"moved">>, 12, ?OTHER_CHANNEL_ID, State1), + {reply, Reply, State3} = guild_voice_connection:voice_state_update( + (request(12, true))#{connection_id => <<"moved">>}, State2 + ), + ?assertMatch(#{success := true, p2p := true, connection_id := <<"moved">>}, Reply), + ?assertEqual(true, p2p_flag(<<"moved">>, State3)), + ?assertEqual(true, p2p_flag(ConnectionA, State3)), + ?assertEqual(#{}, maps:get(pending_voice_connections, State3)) + end). + +client_move_into_a_mesh_without_agreement_is_rejected_test() -> + with_stubs(grant, fun(State0) -> + {Mesh, State1} = join_mesh([10], State0), + State2 = with_sfu_voice_state(<<"moved">>, 12, ?OTHER_CHANNEL_ID, State1), + {reply, Reply, State3} = guild_voice_connection:voice_state_update( + (request(12, undefined))#{connection_id => <<"moved">>}, State2 + ), + ?assertEqual({error, permission_denied, voice_p2p_consent_required}, Reply), + ?assertEqual( + <<"200">>, + maps:get(<<"channel_id">>, maps:get(<<"moved">>, maps:get(voice_states, State3))) + ), + assert_untouched(Mesh, State2, State3) + end). + +signal_relay_allowed_test() -> + with_stubs(grant, fun(State0) -> + {[ConnectionA, ConnectionB], State1} = join_mesh([10, 11], State0), + Data = #{<<"type">> => <<"offer">>, <<"sdp">> => <<"v=0">>, <<"id">> => 7}, + {noreply, State1} = guild_voice_handler:handle_cast( + {voice_signal, signal(<<"sess-10">>, ConnectionB, Data)}, State1 + ), + ?assertEqual( + #{ + <<"guild_id">> => <<"999">>, + <<"channel_id">> => <<"100">>, + <<"from">> => ConnectionA, + <<"user_id">> => <<"10">>, + <<"data">> => Data + }, + receive_voice_signal() + ) + end). + +signal_relay_denied_test() -> + with_stubs(grant, fun(State0) -> + {[ConnectionA, ConnectionB], State1} = join_mesh([10, 11], State0), + Data = #{<<"type">> => <<"candidate">>}, + Denied = [ + signal(<<"sess-12">>, ConnectionB, Data), + signal(<<"sess-10">>, ConnectionA, Data), + signal(<<"sess-10">>, <<"unknown">>, Data), + (signal(<<"sess-10">>, ConnectionB, Data))#{channel_id => 200} + ], + [ + {noreply, State1} = guild_voice_handler:handle_cast({voice_signal, Request}, State1) + || Request <- Denied + ], + State2 = guild_voice_region:switch_to_sfu(?CHANNEL_ID, State1), + {noreply, State2} = guild_voice_handler:handle_cast( + {voice_signal, signal(<<"sess-10">>, ConnectionB, Data)}, State2 + ), + assert_no_voice_signal(), + ?assertEqual(2, length(served_voice_server_updates(State2))) + end). + +signal(SessionId, To, Data) -> + #{session_id => SessionId, channel_id => ?CHANNEL_ID, to => To, data => Data}. + +with_sfu_voice_state(ConnectionId, UserId, ChannelId, State) -> + VoiceState = voice_state_utils:complete_voice_state(#{ + <<"guild_id">> => <<"999">>, + <<"channel_id">> => integer_to_binary(ChannelId), + <<"user_id">> => integer_to_binary(UserId), + <<"connection_id">> => ConnectionId, + <<"session_id">> => session_id(UserId), + <<"member">> => member(UserId) + }), + VoiceStates = maps:get(voice_states, State), + State#{voice_states => VoiceStates#{ConnectionId => VoiceState}}. + +move_request(UserId, ChannelId) -> + #{ + user_id => UserId, + moderator_id => 10, + channel_id => ChannelId, + mute => false, + deaf => false + }. + +assert_converted(Mesh, State) -> + assert_marked_sfu(Mesh, State), + ?assertEqual(lists:sort(Mesh), lists:sort(served_voice_server_updates(State))). + +assert_marked_sfu(Mesh, State) -> + [?assertEqual(false, p2p_flag(ConnectionId, State)) || ConnectionId <- Mesh], + Broadcasts = drain_voice_state_updates(), + [?assert(lists:member({ConnectionId, false}, Broadcasts)) || ConnectionId <- Mesh]. + +assert_untouched(Mesh, Before, After) -> + VoiceStates = maps:get(voice_states, Before), + [ + ?assertEqual( + maps:get(ConnectionId, VoiceStates), + maps:get(ConnectionId, maps:get(voice_states, After)) + ) + || ConnectionId <- Mesh + ], + ?assertEqual( + maps:get(pending_voice_connections, Before), maps:get(pending_voice_connections, After) + ), + Broadcasts = drain_voice_state_updates(), + [?assertNot(lists:member({ConnectionId, false}, Broadcasts)) || ConnectionId <- Mesh], + ?assertEqual([], served_voice_server_updates(After, 0)). + +served_voice_server_updates(State) -> + served_voice_server_updates( + State, + maps:size( + voice_state_utils:channel_voice_states(?CHANNEL_ID, maps:get(voice_states, State)) + ) + ). + +served_voice_server_updates(State, Expected) -> + maps:get(guild_pid, State) ! {serve, State}, + drain_voice_server_updates(Expected). + +drain_voice_server_updates(0) -> + receive + {'$gen_cast', {dispatch, voice_server_update, Payload}} -> + error({unexpected_voice_server_update, Payload}) + after 100 -> + [] + end; +drain_voice_server_updates(Remaining) -> + receive + {'$gen_cast', {dispatch, voice_server_update, Payload}} -> + ?assertEqual(<<"tok">>, maps:get(<<"token">>, Payload)), + ?assertEqual(<<"wss://voice.example">>, maps:get(<<"endpoint">>, Payload)), + ?assertNot(maps:is_key(<<"p2p">>, Payload)), + [maps:get(<<"connection_id">>, Payload) | drain_voice_server_updates(Remaining - 1)] + after 300 -> + [] + end. + +drain_voice_state_updates() -> + receive + {'$gen_cast', {dispatch, voice_state_update, {pre_encoded, Encoded}}} -> + #{<<"connection_id">> := ConnectionId, <<"p2p">> := P2p} = json:decode(Encoded), + [{ConnectionId, P2p} | drain_voice_state_updates()] + after 200 -> + [] + end. + +receive_voice_signal() -> + receive + {'$gen_cast', {dispatch, voice_signal, Payload}} -> Payload + after 1000 -> + error(voice_signal_not_dispatched) + end. + +assert_no_voice_signal() -> + receive + {'$gen_cast', {dispatch, voice_signal, Payload}} -> + error({unexpected_voice_signal, Payload}) + after 100 -> + ok + end. + +p2p_flag(ConnectionId, State) -> + maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(voice_states, State))). + +join_mesh(UserIds, State0) -> + join_mesh(UserIds, ?CHANNEL_ID, State0). + +join_mesh(UserIds, ChannelId, State0) -> + lists:foldl( + fun(UserId, {ConnectionIds, State}) -> + {reply, #{p2p := true, connection_id := ConnectionId}, NewState} = + guild_voice_connection:voice_state_update( + (request(UserId, true))#{channel_id => ChannelId}, State + ), + {ConnectionIds ++ [ConnectionId], NewState} + end, + {[], State0}, + UserIds + ). + +join(UserId, P2p, State) -> + {reply, Reply, NewState} = guild_voice_connection:voice_state_update( + request(UserId, P2p), State + ), + {Reply, NewState}. + +request(UserId, P2p) -> + #{ + user_id => UserId, + channel_id => ?CHANNEL_ID, + session_id => session_id(UserId), + p2p => P2p, + country_code => <<"SE">> + }. + +session_id(UserId) -> + <<"sess-", (integer_to_binary(UserId))/binary>>. + +last_token_request() -> + lists:last(token_requests()). + +token_request_count() -> + length(token_requests()). + +token_requests() -> + [ + Request + || {_Pid, {rpc_client, call, [#{<<"type">> := <<"voice_get_token">>} = Request]}, _Result} <- + meck:history(rpc_client) + ]. + +with_stubs(Mode, Fun) -> + erlang:put(p2p_stub_mode, Mode), + meck:new(rpc_client, [passthrough, no_link, non_strict]), + meck:expect(rpc_client, call, fun rpc_reply/1), + meck:new(guild_voice_server, [passthrough, no_link]), + meck:expect(guild_voice_server, resolve, fun(_GuildId, GuildPid) -> GuildPid end), + GuildPid = spawn(fun guild_stub/0), + Test = self(), + Sink = spawn(fun() -> sink(Test) end), + try + Fun(base_state(GuildPid, Sink)) + after + GuildPid ! stop, + meck:unload(guild_voice_server), + meck:unload(rpc_client), + stop_sink(Sink) + end. + +sink(Test) -> + receive + Message -> + Test ! Message, + sink(Test) + after 30000 -> + ok + end. + +stop_sink(Sink) -> + exit(Sink, kill), + flush_mailbox(). + +flush_mailbox() -> + receive + _Message -> flush_mailbox() + after 0 -> + ok + end. + +rpc_reply(#{<<"type">> := <<"voice_get_token">>} = Request) -> + ConnectionId = maps:get( + <<"connection_id">>, + Request, + <<"conn-", (integer_to_binary(erlang:unique_integer([positive])))/binary>> + ), + case {maps:get(<<"p2p">>, Request, false), erlang:get(p2p_stub_mode)} of + {true, grant} -> + p2p_grant(ConnectionId); + {true, {cap, Max}} -> + case maps:get(<<"p2p_participant_count">>, Request) > Max of + true -> {ok, #{<<"p2p">> => false, <<"declineReason">> => <<"channel_full">>}}; + false -> p2p_grant(ConnectionId) + end; + _ -> + {ok, #{ + <<"token">> => <<"tok">>, + <<"endpoint">> => <<"wss://voice.example">>, + <<"connectionId">> => ConnectionId, + <<"regionId">> => <<"us-east">>, + <<"serverId">> => <<"voice-1">> + }} + end; +rpc_reply(_Request) -> + {ok, #{}}. + +p2p_grant(ConnectionId) -> + {ok, #{ + <<"p2p">> => true, + <<"connectionId">> => ConnectionId, + <<"iceServers">> => [#{<<"urls">> => [<<"stun:stun.example:3478">>]}] + }}. + +guild_stub() -> + receive + {serve, State} -> guild_stub_loop(State); + stop -> ok + after 30000 -> + ok + end. + +guild_stub_loop(State) -> + receive + {serve, NewState} -> + guild_stub_loop(NewState); + {'$gen_call', From, {get_voice_guild_state}} -> + gen_server:reply(From, State), + guild_stub_loop(State); + {'$gen_call', From, _Request} -> + gen_server:reply(From, ok), + guild_stub_loop(State); + stop -> + ok; + _Other -> + guild_stub_loop(State) + after 30000 -> + ok + end. + +base_state(GuildPid, Sink) -> + UserIds = [10, 11, 12, 13, 14], + #{ + id => ?GUILD_ID, + guild_pid => GuildPid, + data => #{ + <<"id">> => <<"999">>, + <<"guild">> => #{<<"owner_id">> => <<"10">>}, + <<"channels">> => [ + #{<<"id">> => <<"100">>, <<"type">> => 2, <<"user_limit">> => 0}, + #{<<"id">> => <<"200">>, <<"type">> => 2, <<"user_limit">> => 0}, + #{ + <<"id">> => <<"300">>, + <<"type">> => 2, + <<"user_limit">> => 0, + <<"rtc_p2p">> => true + } + ], + <<"members">> => [member(UserId) || UserId <- UserIds] + }, + sessions => maps:from_list([ + {session_id(UserId), #{user_id => UserId, pid => Sink, pending_connect => false}} + || UserId <- UserIds + ]), + voice_states => #{}, + pending_voice_connections => #{}, + test_perm_fun => fun(_UserId) -> + constants:view_channel_permission() bor constants:connect_permission() + end + }. + +member(UserId) -> + #{ + <<"user">> => #{<<"id">> => integer_to_binary(UserId)}, + <<"mute">> => false, + <<"deaf">> => false + }. diff --git a/fluxer_gateway/test/session_manager_shard_drain_tests.erl b/fluxer_gateway/test/session_manager_shard_drain_tests.erl index 1d0ba6867..f14623a43 100644 --- a/fluxer_gateway/test/session_manager_shard_drain_tests.erl +++ b/fluxer_gateway/test/session_manager_shard_drain_tests.erl @@ -284,6 +284,40 @@ build_session_data_normalizes_current_user_for_ready_test() -> ?assertEqual(1, maps:get(<<"mention_flags">>, UserData)), ?assertEqual(false, maps:get(<<"is_staff">>, UserData)). +build_session_data_stores_the_geoip_country_code_test() -> + UserDataMap = #{ + <<"id">> => <<"123">>, + <<"username">> => <<"tester">>, + <<"discriminator">> => <<"0001">>, + <<"avatar">> => null, + <<"flags">> => 0 + }, + IdentifyData = #{properties => #{}, token => <<"token">>, presence => null}, + Data = fun(CountryCode) -> + #{ + <<"guilds">> => [], + <<"user">> => UserDataMap, + <<"user_settings">> => null, + <<"geoip_country_code">> => CountryCode + } + end, + CountryCode = fun(Value) -> + maps:get( + geoip_country_code, build_test_session_data(Data(Value), IdentifyData, UserDataMap) + ) + end, + ?assertEqual(<<"SE">>, CountryCode(<<"SE">>)), + ?assertEqual(undefined, CountryCode(null)), + ?assertEqual( + undefined, + maps:get( + geoip_country_code, + build_test_session_data( + maps:remove(<<"geoip_country_code">>, Data(null)), IdentifyData, UserDataMap + ) + ) + ). + build_session_data_filters_guilds_for_identify_shard_test() -> Guild0 = guild_id_for_shard(0, 4, 0), Guild1 = guild_id_for_shard(1, 4, 0), diff --git a/fluxer_gateway/test/voice_p2p_tests.erl b/fluxer_gateway/test/voice_p2p_tests.erl new file mode 100644 index 000000000..af6dca4c9 --- /dev/null +++ b/fluxer_gateway/test/voice_p2p_tests.erl @@ -0,0 +1,242 @@ +%% SPDX-License-Identifier: AGPL-3.0-or-later + +-module(voice_p2p_tests). +-typing([eqwalizer]). + +-include_lib("eunit/include/eunit.hrl"). + +peer(ConnectionId, SessionId, P2p) -> + #{ + <<"connection_id">> => ConnectionId, + <<"session_id">> => SessionId, + <<"user_id">> => <<"10">>, + <<"p2p">> => P2p + }. + +mesh(Count) -> + [ + peer(integer_to_binary(Index), <<"sess-", (integer_to_binary(Index))/binary>>, true) + || Index <- lists:seq(1, Count) + ]. + +agreed_requires_an_explicit_true_from_a_user_test() -> + ?assert(voice_p2p:agreed(true, false)), + ?assertNot(voice_p2p:agreed(true, true)), + ?assertNot(voice_p2p:agreed(false, false)), + ?assertNot(voice_p2p:agreed(undefined, false)), + ?assertNot(voice_p2p:agreed(<<"true">>, false)). + +declined_requires_an_explicit_false_test() -> + ?assert(voice_p2p:declined(false)), + ?assertNot(voice_p2p:declined(true)), + ?assertNot(voice_p2p:declined(undefined)), + ?assertNot(voice_p2p:declined(null)). + +channel_mode_test() -> + ?assertEqual(empty, voice_p2p:channel_mode([])), + ?assertEqual(p2p, voice_p2p:channel_mode(mesh(2))), + ?assertEqual(sfu, voice_p2p:channel_mode([peer(<<"a">>, <<"s">>, false), #{}])), + ?assertEqual(mixed, voice_p2p:channel_mode([peer(<<"a">>, <<"s">>, false) | mesh(1)])). + +pinned_reads_the_channel_flag_test() -> + ?assert(voice_p2p:pinned(#{<<"rtc_p2p">> => true})), + ?assertNot(voice_p2p:pinned(#{<<"rtc_p2p">> => false})), + ?assertNot(voice_p2p:pinned(#{})), + ?assertNot(voice_p2p:pinned(undefined)). + +join_decision_for_a_live_mesh_test() -> + ?assertEqual({p2p, joiner, 2}, voice_p2p:join_decision(true, mesh(1), false, false)), + ?assertEqual({p2p, joiner, 4}, voice_p2p:join_decision(true, mesh(3), false, false)), + ?assertEqual( + {reject, voice_p2p_consent_required}, + voice_p2p:join_decision(false, mesh(3), false, false) + ), + ?assertEqual( + {reject, voice_p2p_consent_required}, + voice_p2p:join_decision(false, mesh(4), false, false) + ), + ?assertEqual( + {reject, voice_channel_full}, voice_p2p:join_decision(true, mesh(4), false, false) + ). + +join_decision_for_an_empty_channel_test() -> + ?assertEqual({p2p, initiator, 1}, voice_p2p:join_decision(true, [], false, false)), + ?assertEqual({p2p, initiator, 1}, voice_p2p:join_decision(true, [], false, true)), + ?assertEqual(sfu, voice_p2p:join_decision(false, [], false, false)), + ?assertEqual(sfu, voice_p2p:join_decision(false, [], false, true)), + ?assertEqual(sfu, voice_p2p:join_decision(true, [], true, false)), + ?assertEqual(sfu, voice_p2p:join_decision(false, [], true, true)). + +join_decision_for_an_sfu_call_ignores_the_flag_test() -> + Sfu = [peer(<<"a">>, <<"s">>, false)], + ?assertEqual(sfu, voice_p2p:join_decision(true, Sfu, false, false)), + ?assertEqual(sfu, voice_p2p:join_decision(false, Sfu, false, true)). + +admit_test() -> + Grant = {p2p, <<"conn">>, [], 2}, + Token = #{token => <<"tok">>}, + ?assertEqual(Grant, voice_p2p:admit({p2p, initiator, 1}, Grant)), + ?assertEqual(Grant, voice_p2p:admit({p2p, joiner, 3}, Grant)), + ?assertEqual(sfu, voice_p2p:admit({p2p, initiator, 1}, Token)), + ?assertEqual({reject, voice_p2p_unavailable}, voice_p2p:admit({p2p, joiner, 3}, Token)), + ?assertEqual(sfu, voice_p2p:admit(sfu, Token)), + ?assertEqual({reject, voice_token_failed}, voice_p2p:admit(sfu, Grant)). + +admit_maps_a_size_decline_to_channel_full_test() -> + Declined = {declined, voice_channel_full}, + ?assertEqual({reject, voice_channel_full}, voice_p2p:admit({p2p, joiner, 3}, Declined)), + ?assertEqual({reject, voice_channel_full}, voice_p2p:admit({p2p, initiator, 1}, Declined)), + ?assertEqual({reject, voice_token_failed}, voice_p2p:admit(sfu, Declined)). + +invariant_violation_test() -> + ?assertEqual(ok, voice_p2p:invariant_violation([])), + ?assertEqual(ok, voice_p2p:invariant_violation(mesh(4))), + ?assertEqual({error, voice_channel_full}, voice_p2p:invariant_violation(mesh(5))), + ?assertEqual( + {error, voice_p2p_consent_required}, + voice_p2p:invariant_violation([peer(<<"a">>, <<"s">>, false) | mesh(1)]) + ), + ?assertEqual(ok, voice_p2p:invariant_violation([peer(<<"a">>, <<"s">>, false)])). + +invariant_violation_holds_a_mesh_to_the_configured_cap_test() -> + ?assertEqual(ok, voice_p2p:invariant_violation(mesh(2), 2)), + ?assertEqual({error, voice_channel_full}, voice_p2p:invariant_violation(mesh(3), 2)), + ?assertEqual(ok, voice_p2p:invariant_violation(mesh(4), undefined)), + ?assertEqual({error, voice_channel_full}, voice_p2p:invariant_violation(mesh(5), 9)), + ?assertEqual(ok, voice_p2p:invariant_violation([peer(<<"a">>, <<"s">>, false)], 1)). + +reports_rejection_test() -> + ?assert(voice_p2p:reports_rejection(voice_p2p_consent_required)), + ?assert(voice_p2p:reports_rejection(voice_p2p_unavailable)), + ?assert(voice_p2p:reports_rejection(voice_channel_full)), + ?assertNot(voice_p2p:reports_rejection(voice_token_failed)). + +to_sfu_clears_the_flag_and_bumps_the_version_test() -> + VoiceState = (peer(<<"a">>, <<"s">>, true))#{<<"version">> => 3}, + Converted = voice_p2p:to_sfu(VoiceState), + ?assertEqual(false, maps:get(<<"p2p">>, Converted)), + ?assertEqual(4, maps:get(<<"version">>, Converted)). + +add_to_token_request_test() -> + Request = #{<<"type">> => <<"voice_get_token">>}, + ?assertEqual(Request, voice_p2p:add_to_token_request(Request, sfu, <<"SE">>)), + ?assertEqual( + Request#{ + <<"p2p">> => true, + <<"p2p_initiator">> => true, + <<"p2p_participant_count">> => 1, + <<"country_code">> => <<"SE">> + }, + voice_p2p:add_to_token_request(Request, {p2p, initiator, 1}, <<"SE">>) + ), + ?assertEqual( + Request#{ + <<"p2p">> => true, <<"p2p_initiator">> => false, <<"p2p_participant_count">> => 3 + }, + voice_p2p:add_to_token_request(Request, {p2p, joiner, 3}, undefined) + ). + +grant_reads_a_p2p_grant_test() -> + Stun = #{<<"urls">> => [<<"stun:stun.example:3478">>], <<"ignored">> => true}, + Data = #{<<"p2p">> => true, <<"connectionId">> => <<"conn-1">>, <<"iceServers">> => [Stun]}, + ?assertEqual( + {p2p, <<"conn-1">>, [#{<<"urls">> => [<<"stun:stun.example:3478">>]}], 4}, + voice_p2p:grant(Data) + ). + +grant_reads_the_configured_cap_test() -> + Data = #{<<"p2p">> => true, <<"connectionId">> => <<"conn-1">>, <<"iceServers">> => []}, + ?assertEqual( + {p2p, <<"conn-1">>, [], 3}, voice_p2p:grant(Data#{<<"maxParticipants">> => 3}) + ), + ?assertEqual( + {p2p, <<"conn-1">>, [], 4}, voice_p2p:grant(Data#{<<"maxParticipants">> => 9}) + ), + ?assertEqual( + {p2p, <<"conn-1">>, [], 4}, voice_p2p:grant(Data#{<<"maxParticipants">> => 0}) + ). + +grant_keeps_only_urls_of_each_ice_server_test() -> + Server = #{ + <<"urls">> => [<<"stun:stun.example:3478">>], + <<"username">> => <<"user">>, + <<"credential">> => <<"secret">> + }, + Data = #{ + <<"p2p">> => true, <<"connectionId">> => <<"conn-1">>, <<"iceServers">> => [Server] + }, + ?assertEqual( + {p2p, <<"conn-1">>, [#{<<"urls">> => [<<"stun:stun.example:3478">>]}], 4}, + voice_p2p:grant(Data) + ). + +grant_reads_a_size_decline_test() -> + ?assertEqual( + {declined, voice_channel_full}, + voice_p2p:grant(#{<<"p2p">> => false, <<"declineReason">> => <<"channel_full">>}) + ), + ?assertEqual( + sfu, voice_p2p:grant(#{<<"p2p">> => false, <<"declineReason">> => <<"unknown">>}) + ). + +grant_reads_a_token_response_as_sfu_test() -> + ?assertEqual( + sfu, + voice_p2p:grant(#{ + <<"token">> => <<"tok">>, + <<"endpoint">> => <<"wss://voice.example">>, + <<"connectionId">> => <<"conn-1">> + }) + ). + +voice_server_update_shape_test() -> + IceServers = [#{<<"urls">> => [<<"stun:stun.example:3478">>]}], + ?assertEqual( + #{ + <<"p2p">> => true, + <<"ice_servers">> => IceServers, + <<"connection_id">> => <<"conn-1">>, + <<"channel_id">> => <<"100">>, + <<"guild_id">> => <<"999">> + }, + voice_p2p:voice_server_update(<<"conn-1">>, 100, 999, IceServers) + ), + ?assertNot( + maps:is_key( + <<"guild_id">>, voice_p2p:voice_server_update(<<"conn-1">>, 100, null, IceServers) + ) + ). + +signal_route_allows_a_mesh_peer_test() -> + [Sender, Target] = mesh(2), + ?assertEqual({ok, Sender, Target}, voice_p2p:signal_route(mesh(2), <<"sess-1">>, <<"2">>)). + +signal_route_denies_a_sender_outside_the_mesh_test() -> + ?assertEqual(error, voice_p2p:signal_route(mesh(2), <<"sess-9">>, <<"2">>)). + +signal_route_denies_an_unknown_target_test() -> + ?assertEqual(error, voice_p2p:signal_route(mesh(2), <<"sess-1">>, <<"9">>)). + +signal_route_denies_the_senders_own_connection_test() -> + ?assertEqual(error, voice_p2p:signal_route(mesh(2), <<"sess-1">>, <<"1">>)). + +signal_route_denies_sfu_voice_states_test() -> + Sfu = [peer(<<"1">>, <<"sess-1">>, false), peer(<<"2">>, <<"sess-2">>, false)], + ?assertEqual(error, voice_p2p:signal_route(Sfu, <<"sess-1">>, <<"2">>)), + Mixed = [peer(<<"1">>, <<"sess-1">>, true), peer(<<"2">>, <<"sess-2">>, false)], + ?assertEqual(error, voice_p2p:signal_route(Mixed, <<"sess-1">>, <<"2">>)). + +signal_payload_shape_test() -> + [Sender | _] = mesh(1), + Data = #{<<"type">> => <<"offer">>, <<"sdp">> => <<"v=0">>}, + ?assertEqual( + #{ + <<"guild_id">> => <<"999">>, + <<"channel_id">> => <<"100">>, + <<"from">> => <<"1">>, + <<"user_id">> => <<"10">>, + <<"data">> => Data + }, + voice_p2p:signal_payload(999, 100, Sender, Data) + ), + ?assertNot(maps:is_key(<<"guild_id">>, voice_p2p:signal_payload(null, 100, Sender, Data))). diff --git a/packages/config/src/MasterConfig.ts b/packages/config/src/MasterConfig.ts index 79b4f5dc0..1ba134780 100644 --- a/packages/config/src/MasterConfig.ts +++ b/packages/config/src/MasterConfig.ts @@ -205,6 +205,7 @@ export interface MasterConfig { latitude: number; longitude: number; }; + p2p_stun_urls?: Array; }; search: { engine: 'elasticsearch' | 'meilisearch'; diff --git a/packages/config/src/__tests__/EnvironmentOverrides.test.ts b/packages/config/src/__tests__/EnvironmentOverrides.test.ts index d422e98a4..f5d2fdc90 100644 --- a/packages/config/src/__tests__/EnvironmentOverrides.test.ts +++ b/packages/config/src/__tests__/EnvironmentOverrides.test.ts @@ -86,6 +86,7 @@ describe('buildNamedFluxerEnvOverrides', () => { FLUXER_API_IP_BAN_EXEMPT_IPS: blank, FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: blank, FLUXER_LIVEKIT_DEFAULT_REGION: blank, + FLUXER_VOICE_P2P_STUN_URLS: blank, FLUXER_AUTH_BLUESKY_KEYS: blank, FLUXER_EMAIL_FROM_NAME: blank, FLUXER_EMAIL_REPLY_TO_EMAIL: blank, @@ -146,6 +147,20 @@ describe('buildNamedFluxerEnvOverrides', () => { }); }); + test('maps the peer-to-peer STUN urls onto integrations.voice', () => { + expect( + buildNamedFluxerEnvOverrides({ + FLUXER_VOICE_P2P_STUN_URLS: 'stun:stun.example.com:3478, stun:stun2.example.com:3478', + }), + ).toEqual({ + integrations: { + voice: { + p2p_stun_urls: ['stun:stun.example.com:3478', 'stun:stun2.example.com:3478'], + }, + }, + }); + }); + test('rejects malformed JSON for a JSON-shaped override', () => { expect(() => buildNamedFluxerEnvOverrides({FLUXER_LIVEKIT_DEFAULT_REGION: '{bad'})).toThrow( 'FLUXER_LIVEKIT_DEFAULT_REGION must be valid JSON', diff --git a/packages/config/src/config_loader/EnvironmentOverrides.ts b/packages/config/src/config_loader/EnvironmentOverrides.ts index 77ff42805..dbd4931cf 100644 --- a/packages/config/src/config_loader/EnvironmentOverrides.ts +++ b/packages/config/src/config_loader/EnvironmentOverrides.ts @@ -170,6 +170,7 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record = { FLUXER_LIVEKIT_URL: {path: ['integrations', 'voice', 'url']}, FLUXER_LIVEKIT_INTERNAL_URL: {path: ['integrations', 'voice', 'internal_url']}, FLUXER_LIVEKIT_DEFAULT_REGION: {path: ['integrations', 'voice', 'default_region'], parse: parseJsonObject}, + FLUXER_VOICE_P2P_STUN_URLS: {path: ['integrations', 'voice', 'p2p_stun_urls'], parse: parseCsv}, FLUXER_SEARCH_ENGINE: {path: ['integrations', 'search', 'engine']}, FLUXER_SEARCH_URL: {path: ['integrations', 'search', 'url']}, FLUXER_SEARCH_API_KEY: {path: ['integrations', 'search', 'api_key']}, diff --git a/packages/constants/src/GatewayConstants.ts b/packages/constants/src/GatewayConstants.ts index 971e14a70..20d8f14be 100644 --- a/packages/constants/src/GatewayConstants.ts +++ b/packages/constants/src/GatewayConstants.ts @@ -16,6 +16,7 @@ export const GatewayOpcodes = { HELLO: 10, HEARTBEAT_ACK: 11, GATEWAY_ERROR: 12, + VOICE_SIGNAL: 13, LAZY_REQUEST: 14, REQUEST_GUILD_COUNTS: 15, REQUEST_CHANNEL_MEMBER_COUNTS: 16, @@ -62,6 +63,8 @@ export const GatewayErrorCodes = { VOICE_MEMBER_NOT_FOUND: 'VOICE_MEMBER_NOT_FOUND', VOICE_MEMBER_TIMED_OUT: 'VOICE_MEMBER_TIMED_OUT', VOICE_MISSING_CONNECTION_ID: 'VOICE_MISSING_CONNECTION_ID', + VOICE_P2P_CONSENT_REQUIRED: 'VOICE_P2P_CONSENT_REQUIRED', + VOICE_P2P_UNAVAILABLE: 'VOICE_P2P_UNAVAILABLE', VOICE_PERMISSION_DENIED: 'VOICE_PERMISSION_DENIED', VOICE_TOKEN_FAILED: 'VOICE_TOKEN_FAILED', VOICE_UNCLAIMED_ACCOUNT: 'VOICE_UNCLAIMED_ACCOUNT', diff --git a/packages/constants/src/LimitConstants.ts b/packages/constants/src/LimitConstants.ts index 0f5d1282d..543ffe481 100644 --- a/packages/constants/src/LimitConstants.ts +++ b/packages/constants/src/LimitConstants.ts @@ -18,6 +18,7 @@ export const VOICE_CHANNEL_CAMERA_USER_LIMIT = 25; export const VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT = 5; export const VOICE_CHANNEL_CONNECTION_LIMIT_MIN = 1; export const VOICE_CHANNEL_CONNECTION_LIMIT_MAX = 100; +export const VOICE_P2P_MAX_PARTICIPANTS = 4; export const CHANNEL_RATE_LIMIT_PER_USER_MIN = 0; export const CHANNEL_RATE_LIMIT_PER_USER_MAX = 21600; export const CHANNEL_TOPIC_MIN_LENGTH = 1; diff --git a/packages/schema/proto/fluxer/user/preferences/v1/preferences.proto b/packages/schema/proto/fluxer/user/preferences/v1/preferences.proto index 391865712..d35c1125f 100644 --- a/packages/schema/proto/fluxer/user/preferences/v1/preferences.proto +++ b/packages/schema/proto/fluxer/user/preferences/v1/preferences.proto @@ -222,6 +222,7 @@ message InputMonitoringPromptsState { message VoicePromptsState { bool skip_hide_own_camera_confirm = 1; bool skip_hide_own_screenshare_confirm = 2; + bool skip_p2p_join_confirm = 3; } message AnnouncementPromptsState { diff --git a/packages/schema/src/domains/admin/AdminSchemas.ts b/packages/schema/src/domains/admin/AdminSchemas.ts index 7bc98d49b..9c5001cc1 100644 --- a/packages/schema/src/domains/admin/AdminSchemas.ts +++ b/packages/schema/src/domains/admin/AdminSchemas.ts @@ -25,6 +25,7 @@ import { InstanceBillingUpdateRequest, } from '@fluxer/schema/src/domains/admin/InstanceBillingSchemas'; import {PushRelayConfigResponse, PushRelayConfigUpdateRequest} from '@fluxer/schema/src/domains/admin/PushRelaySchemas'; +import {VoiceP2pConfigResponse, VoiceP2pConfigUpdateRequest} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; import { ExperimentDeliveryConfigResponse, ExperimentDeliveryConfigUpdateRequest, @@ -701,6 +702,7 @@ export const InstanceConfigResponse = z.object({ gateway_rollout: GatewayRolloutConfigResponse, push_relay: PushRelayConfigResponse, domain_migration: DomainMigrationConfigResponse, + voice_p2p: VoiceP2pConfigResponse, captcha: CaptchaConfigResponse, experiment_delivery: ExperimentDeliveryConfigResponse, registration: InstanceRegistrationResponse, @@ -735,6 +737,7 @@ export const InstanceConfigUpdateRequest = z.object({ gateway_rollout: GatewayRolloutConfigUpdateRequest.nullish(), push_relay: PushRelayConfigUpdateRequest.nullish(), domain_migration: DomainMigrationConfigUpdateRequest.nullish(), + voice_p2p: VoiceP2pConfigUpdateRequest.nullish(), captcha: CaptchaConfigUpdateRequest.nullish(), experiment_delivery: ExperimentDeliveryConfigUpdateRequest.nullish(), registration: z diff --git a/packages/schema/src/domains/admin/DomainMigrationSchemas.test.ts b/packages/schema/src/domains/admin/DomainMigrationSchemas.test.ts index ce11f4fbe..3d07210f3 100644 --- a/packages/schema/src/domains/admin/DomainMigrationSchemas.test.ts +++ b/packages/schema/src/domains/admin/DomainMigrationSchemas.test.ts @@ -12,7 +12,7 @@ import { import {type ExperimentTargeting, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket'; import {describe, expect, test} from 'vitest'; -const NO_TARGETING: ExperimentTargeting = {memberGuildIds: new Set(), premium: false}; +const NO_TARGETING: ExperimentTargeting = {memberGuildIds: new Set(), premium: false, countryCode: null}; const TARGETED_USER_ID = '1000000000000000001'; const OTHER_USER_ID = '1000000000000000002'; @@ -246,6 +246,7 @@ describe('resolveDomainMigrationAssignment guild targeting', () => { const MEMBER_GUILDS: ExperimentTargeting = { memberGuildIds: new Set(['3000000000000000009', INCLUDED_GUILD_ID]), premium: false, + countryCode: null, }; test('serves members of an included guild at zero rollout', () => { @@ -270,7 +271,7 @@ describe('resolveDomainMigrationAssignment guild targeting', () => { }); describe('resolveDomainMigrationAssignment premium targeting', () => { - const PREMIUM: ExperimentTargeting = {memberGuildIds: new Set(), premium: true}; + const PREMIUM: ExperimentTargeting = {memberGuildIds: new Set(), premium: true, countryCode: null}; test('serves premium users only when the switch is on', () => { const on = createConfig({enabled: true, include_premium_users: true}); @@ -290,3 +291,60 @@ describe('resolveDomainMigrationAssignment premium targeting', () => { expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, PREMIUM)).toEqual({enabled: false}); }); }); + +describe('resolveDomainMigrationAssignment country targeting', () => { + const IN_SWEDEN: ExperimentTargeting = {...NO_TARGETING, countryCode: 'SE'}; + const IN_BRAZIL: ExperimentTargeting = {...NO_TARGETING, countryCode: 'BR'}; + + test('limits the percentage rollout to the listed countries', () => { + const config = createConfig({enabled: true, rollout_basis_points: 10000, rollout_country_codes: ['SE', 'NO']}); + expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, IN_SWEDEN)).toEqual({enabled: true}); + expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, IN_BRAZIL)).toEqual({enabled: false}); + expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false}); + }); + + test('ignores the country while the list is empty', () => { + const config = createConfig({enabled: true, rollout_basis_points: 10000}); + expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, IN_BRAZIL)).toEqual({enabled: true}); + expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: true}); + }); + + test('still buckets users inside a listed country', () => { + const config = createConfig({enabled: true, rollout_basis_points: 2500, rollout_country_codes: ['SE']}); + for (const userId of syntheticUserIds(200)) { + expect(resolveDomainMigrationAssignment(config, userId, IN_SWEDEN).enabled).toBe( + experimentBucket(userId, config.rollout_salt) < 2500, + ); + } + }); + + test('keeps included users, included guilds and premium users outside the listed countries', () => { + const guildId = '3000000000000000001'; + const config = createConfig({ + enabled: true, + rollout_country_codes: ['SE'], + included_user_ids: [TARGETED_USER_ID], + included_guild_ids: [guildId], + include_premium_users: true, + }); + const otherUserId = '1000000000000000007'; + expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, IN_BRAZIL)).toEqual({enabled: true}); + expect( + resolveDomainMigrationAssignment(config, otherUserId, {...IN_BRAZIL, memberGuildIds: new Set([guildId])}), + ).toEqual({enabled: true}); + expect(resolveDomainMigrationAssignment(config, otherUserId, {...IN_BRAZIL, premium: true})).toEqual({ + enabled: true, + }); + expect(resolveDomainMigrationAssignment(config, otherUserId, IN_BRAZIL)).toEqual({enabled: false}); + }); + + test('keeps exclusions ahead of a listed country', () => { + const config = createConfig({ + enabled: true, + rollout_basis_points: 10000, + rollout_country_codes: ['SE'], + excluded_user_ids: [TARGETED_USER_ID], + }); + expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, IN_SWEDEN)).toEqual({enabled: false}); + }); +}); diff --git a/packages/schema/src/domains/admin/DomainMigrationSchemas.ts b/packages/schema/src/domains/admin/DomainMigrationSchemas.ts index 6eb3f0a31..2910bcd9d 100644 --- a/packages/schema/src/domains/admin/DomainMigrationSchemas.ts +++ b/packages/schema/src/domains/admin/DomainMigrationSchemas.ts @@ -2,9 +2,11 @@ import { EXPERIMENT_BUCKET_RESOLUTION, + ExperimentRolloutCountryCodesSchema, type ExperimentTargeting, experimentAudienceIncludes, experimentBucket, + experimentRolloutCountryIncludes, } from '@fluxer/schema/src/domains/experiment/ExperimentBucket'; import {z} from 'zod'; @@ -23,6 +25,7 @@ const domainMigrationConfigFields = { enabled: z.boolean(), config_version: z.number().int().min(0), rollout_basis_points: z.number().int().min(0).max(DOMAIN_MIGRATION_ROLLOUT_BASIS_POINTS_MAX), + rollout_country_codes: ExperimentRolloutCountryCodesSchema, rollout_salt: z.string().trim().min(1).max(64).regex(DOMAIN_MIGRATION_SALT_PATTERN), included_user_ids: DomainMigrationTargetedUserIdsSchema, included_guild_ids: DomainMigrationTargetedUserIdsSchema, @@ -36,6 +39,7 @@ export const DomainMigrationConfigSchema = z.object({ enabled: domainMigrationConfigFields.enabled.default(false), config_version: domainMigrationConfigFields.config_version.default(0), rollout_basis_points: domainMigrationConfigFields.rollout_basis_points.default(0), + rollout_country_codes: domainMigrationConfigFields.rollout_country_codes.default([]), rollout_salt: domainMigrationConfigFields.rollout_salt.default(DEFAULT_DOMAIN_MIGRATION_SALT), included_user_ids: domainMigrationConfigFields.included_user_ids.default([]), included_guild_ids: domainMigrationConfigFields.included_guild_ids.default([]), @@ -79,6 +83,7 @@ export function resolveDomainMigrationAssignment( if (config.excluded_user_ids.includes(userId)) return {...INERT_DOMAIN_MIGRATION_ASSIGNMENT}; if (config.included_user_ids.includes(userId)) return {enabled: true}; if (experimentAudienceIncludes(config, targeting)) return {enabled: true}; + if (!experimentRolloutCountryIncludes(config, targeting)) return {...INERT_DOMAIN_MIGRATION_ASSIGNMENT}; return {enabled: experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points}; } diff --git a/packages/schema/src/domains/admin/VoiceP2pSchemas.test.ts b/packages/schema/src/domains/admin/VoiceP2pSchemas.test.ts new file mode 100644 index 000000000..959d66bda --- /dev/null +++ b/packages/schema/src/domains/admin/VoiceP2pSchemas.test.ts @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { + DEFAULT_VOICE_P2P_CONFIG, + resolveVoiceP2pAssignment, + type VoiceP2pConfig, + VoiceP2pConfigSchema, + VoiceP2pConfigUpdateRequest, +} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; +import {type ExperimentTargeting, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket'; +import {describe, expect, test} from 'vitest'; + +const NO_TARGETING: ExperimentTargeting = {memberGuildIds: new Set(), premium: false, countryCode: null}; + +const TARGETED_USER_ID = '1000000000000000001'; + +const SELECTED = {enabled: true, max_participants: 2}; +const UNSELECTED = {enabled: false, max_participants: 2}; + +function createConfig(overrides: Partial = {}): VoiceP2pConfig { + return {...DEFAULT_VOICE_P2P_CONFIG, included_user_ids: [], excluded_user_ids: [], ...overrides}; +} + +function syntheticUserIds(count: number): Array { + return Array.from({length: count}, (_, index) => (1400000000000000000n + BigInt(index)).toString()); +} + +describe('voice p2p configuration', () => { + test('defaults to disabled', () => { + expect(VoiceP2pConfigSchema.parse({})).toEqual({ + enabled: false, + config_version: 0, + rollout_basis_points: 0, + rollout_country_codes: [], + rollout_salt: 'voice-p2p-v1', + included_user_ids: [], + excluded_user_ids: [], + included_guild_ids: [], + include_premium_users: false, + max_participants: 2, + }); + }); + + test('limits max_participants to 2 through 4', () => { + expect(VoiceP2pConfigUpdateRequest.safeParse({max_participants: 4}).data).toEqual({max_participants: 4}); + expect(VoiceP2pConfigUpdateRequest.safeParse({max_participants: 1}).success).toBe(false); + expect(VoiceP2pConfigUpdateRequest.safeParse({max_participants: 5}).success).toBe(false); + expect(VoiceP2pConfigUpdateRequest.safeParse({max_participants: 2.5}).success).toBe(false); + }); + + test('strips config_version from admin updates', () => { + expect(VoiceP2pConfigUpdateRequest.safeParse({config_version: 3}).data).toEqual({}); + expect(VoiceP2pConfigUpdateRequest.safeParse({rollout_basis_points: 10001}).success).toBe(false); + }); +}); + +describe('resolveVoiceP2pAssignment', () => { + test('reports the configured cap whether or not the caller is selected', () => { + const config = createConfig({enabled: true, max_participants: 4, included_user_ids: [TARGETED_USER_ID]}); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({ + enabled: true, + max_participants: 4, + }); + expect(resolveVoiceP2pAssignment(config, '1000000000000000007', NO_TARGETING)).toEqual({ + enabled: false, + max_participants: 4, + }); + expect(resolveVoiceP2pAssignment({...config, enabled: false}, TARGETED_USER_ID, NO_TARGETING)).toEqual({ + enabled: false, + max_participants: 4, + }); + }); + + test('serves nobody while disabled, even included users', () => { + const config = createConfig({rollout_basis_points: 10000, included_user_ids: [TARGETED_USER_ID]}); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual(UNSELECTED); + }); + + test('applies exclusions before inclusions', () => { + const config = createConfig({ + enabled: true, + included_user_ids: [TARGETED_USER_ID], + excluded_user_ids: [TARGETED_USER_ID], + }); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual(UNSELECTED); + }); + + test('serves included users at zero rollout', () => { + const config = createConfig({enabled: true, included_user_ids: [TARGETED_USER_ID]}); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual(SELECTED); + }); + + test('buckets the rollout by salt and user id', () => { + const config = createConfig({enabled: true, rollout_basis_points: 2500}); + for (const userId of syntheticUserIds(200)) { + expect(resolveVoiceP2pAssignment(config, userId, NO_TARGETING).enabled).toBe( + experimentBucket(userId, config.rollout_salt) < 2500, + ); + } + }); +}); + +describe('resolveVoiceP2pAssignment guild targeting', () => { + const INCLUDED_GUILD_ID = '3000000000000000001'; + const MEMBER_GUILDS: ExperimentTargeting = { + memberGuildIds: new Set(['3000000000000000009', INCLUDED_GUILD_ID]), + premium: false, + countryCode: null, + }; + + test('serves members of an included guild at zero rollout', () => { + const config = createConfig({enabled: true, included_guild_ids: [INCLUDED_GUILD_ID]}); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual(SELECTED); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual(UNSELECTED); + }); + + test('keeps user exclusions ahead of guild membership', () => { + const config = createConfig({ + enabled: true, + included_guild_ids: [INCLUDED_GUILD_ID], + excluded_user_ids: [TARGETED_USER_ID], + }); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual(UNSELECTED); + }); + + test('serves no guild members while disabled', () => { + const config = createConfig({included_guild_ids: [INCLUDED_GUILD_ID]}); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual(UNSELECTED); + }); +}); + +describe('resolveVoiceP2pAssignment premium targeting', () => { + const PREMIUM: ExperimentTargeting = {memberGuildIds: new Set(), premium: true, countryCode: null}; + + test('serves premium users only when the switch is on', () => { + const on = createConfig({enabled: true, include_premium_users: true}); + expect(resolveVoiceP2pAssignment(on, TARGETED_USER_ID, PREMIUM)).toEqual(SELECTED); + expect(resolveVoiceP2pAssignment(on, TARGETED_USER_ID, NO_TARGETING)).toEqual(UNSELECTED); + expect(resolveVoiceP2pAssignment(createConfig({enabled: true}), TARGETED_USER_ID, PREMIUM)).toEqual(UNSELECTED); + }); + + test('keeps user exclusions ahead of the premium switch', () => { + const config = createConfig({ + enabled: true, + include_premium_users: true, + excluded_user_ids: [TARGETED_USER_ID], + }); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, PREMIUM)).toEqual(UNSELECTED); + }); +}); + +describe('resolveVoiceP2pAssignment country targeting', () => { + const IN_SWEDEN: ExperimentTargeting = {...NO_TARGETING, countryCode: 'SE'}; + const IN_BRAZIL: ExperimentTargeting = {...NO_TARGETING, countryCode: 'BR'}; + + test('limits the percentage rollout to the listed countries', () => { + const config = createConfig({enabled: true, rollout_basis_points: 10000, rollout_country_codes: ['SE', 'NO']}); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, IN_SWEDEN)).toEqual(SELECTED); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, IN_BRAZIL)).toEqual(UNSELECTED); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual(UNSELECTED); + }); + + test('ignores the country while the list is empty', () => { + const config = createConfig({enabled: true, rollout_basis_points: 10000}); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, IN_BRAZIL)).toEqual(SELECTED); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual(SELECTED); + }); + + test('still buckets users inside a listed country', () => { + const config = createConfig({enabled: true, rollout_basis_points: 2500, rollout_country_codes: ['SE']}); + for (const userId of syntheticUserIds(200)) { + expect(resolveVoiceP2pAssignment(config, userId, IN_SWEDEN).enabled).toBe( + experimentBucket(userId, config.rollout_salt) < 2500, + ); + } + }); + + test('keeps included users, included guilds and premium users outside the listed countries', () => { + const guildId = '3000000000000000001'; + const config = createConfig({ + enabled: true, + rollout_country_codes: ['SE'], + included_user_ids: [TARGETED_USER_ID], + included_guild_ids: [guildId], + include_premium_users: true, + }); + const otherUserId = '1000000000000000007'; + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, IN_BRAZIL)).toEqual(SELECTED); + expect(resolveVoiceP2pAssignment(config, otherUserId, {...IN_BRAZIL, memberGuildIds: new Set([guildId])})).toEqual( + SELECTED, + ); + expect(resolveVoiceP2pAssignment(config, otherUserId, {...IN_BRAZIL, premium: true})).toEqual(SELECTED); + expect(resolveVoiceP2pAssignment(config, otherUserId, IN_BRAZIL)).toEqual(UNSELECTED); + }); + + test('keeps exclusions ahead of a listed country', () => { + const config = createConfig({ + enabled: true, + rollout_basis_points: 10000, + rollout_country_codes: ['SE'], + excluded_user_ids: [TARGETED_USER_ID], + }); + expect(resolveVoiceP2pAssignment(config, TARGETED_USER_ID, IN_SWEDEN)).toEqual(UNSELECTED); + }); +}); diff --git a/packages/schema/src/domains/admin/VoiceP2pSchemas.ts b/packages/schema/src/domains/admin/VoiceP2pSchemas.ts new file mode 100644 index 000000000..653ff1b96 --- /dev/null +++ b/packages/schema/src/domains/admin/VoiceP2pSchemas.ts @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {VOICE_P2P_MAX_PARTICIPANTS} from '@fluxer/constants/src/LimitConstants'; +import { + EXPERIMENT_BUCKET_RESOLUTION, + ExperimentRolloutCountryCodesSchema, + type ExperimentTargeting, + experimentAudienceIncludes, + experimentBucket, + experimentRolloutCountryIncludes, +} from '@fluxer/schema/src/domains/experiment/ExperimentBucket'; +import {z} from 'zod'; + +const VOICE_P2P_ROLLOUT_BASIS_POINTS_MAX = EXPERIMENT_BUCKET_RESOLUTION; +const VOICE_P2P_MAX_TARGETED_USERS = 1000; +const DEFAULT_VOICE_P2P_SALT = 'voice-p2p-v1'; +const VOICE_P2P_MIN_PARTICIPANTS = 2; + +const VOICE_P2P_SALT_PATTERN = /^[\x20-\x7e]+$/u; + +const VoiceP2pTargetIdSchema = z.string().regex(/^\d{1,20}$/u); +const VoiceP2pTargetedUserIdsSchema = z.array(VoiceP2pTargetIdSchema).max(VOICE_P2P_MAX_TARGETED_USERS); + +const voiceP2pConfigFields = { + enabled: z.boolean(), + config_version: z.number().int().min(0), + rollout_basis_points: z.number().int().min(0).max(VOICE_P2P_ROLLOUT_BASIS_POINTS_MAX), + rollout_country_codes: ExperimentRolloutCountryCodesSchema, + rollout_salt: z.string().trim().min(1).max(64).regex(VOICE_P2P_SALT_PATTERN), + included_user_ids: VoiceP2pTargetedUserIdsSchema, + included_guild_ids: VoiceP2pTargetedUserIdsSchema, + include_premium_users: z.boolean(), + excluded_user_ids: VoiceP2pTargetedUserIdsSchema, + max_participants: z.number().int().min(VOICE_P2P_MIN_PARTICIPANTS).max(VOICE_P2P_MAX_PARTICIPANTS), +}; + +export const VoiceP2pConfigSchema = z.object({ + enabled: voiceP2pConfigFields.enabled.default(false), + config_version: voiceP2pConfigFields.config_version.default(0), + rollout_basis_points: voiceP2pConfigFields.rollout_basis_points.default(0), + rollout_country_codes: voiceP2pConfigFields.rollout_country_codes.default([]), + rollout_salt: voiceP2pConfigFields.rollout_salt.default(DEFAULT_VOICE_P2P_SALT), + included_user_ids: voiceP2pConfigFields.included_user_ids.default([]), + included_guild_ids: voiceP2pConfigFields.included_guild_ids.default([]), + include_premium_users: voiceP2pConfigFields.include_premium_users.default(false), + excluded_user_ids: voiceP2pConfigFields.excluded_user_ids.default([]), + max_participants: voiceP2pConfigFields.max_participants.default(VOICE_P2P_MIN_PARTICIPANTS), +}); + +export type VoiceP2pConfig = z.infer; + +export const DEFAULT_VOICE_P2P_CONFIG: VoiceP2pConfig = VoiceP2pConfigSchema.parse({}); + +export const VoiceP2pConfigUpdateRequest = z.object(voiceP2pConfigFields).omit({config_version: true}).partial(); + +export type VoiceP2pConfigUpdateRequest = z.infer; + +export const VoiceP2pConfigResponse = VoiceP2pConfigSchema; + +export type VoiceP2pConfigResponse = z.infer; + +export const VoiceP2pAssignmentResponse = z.object({ + enabled: voiceP2pConfigFields.enabled, + max_participants: voiceP2pConfigFields.max_participants, +}); + +export type VoiceP2pAssignmentResponse = z.infer; + +export const INERT_VOICE_P2P_ASSIGNMENT: VoiceP2pAssignmentResponse = { + enabled: false, + max_participants: DEFAULT_VOICE_P2P_CONFIG.max_participants, +}; + +export function resolveVoiceP2pAssignment( + config: VoiceP2pConfig, + userId: string, + targeting: ExperimentTargeting, +): VoiceP2pAssignmentResponse { + return {enabled: voiceP2pSelects(config, userId, targeting), max_participants: config.max_participants}; +} + +function voiceP2pSelects(config: VoiceP2pConfig, userId: string, targeting: ExperimentTargeting): boolean { + if (!config.enabled) return false; + if (config.excluded_user_ids.includes(userId)) return false; + if (config.included_user_ids.includes(userId)) return true; + if (experimentAudienceIncludes(config, targeting)) return true; + if (!experimentRolloutCountryIncludes(config, targeting)) return false; + return experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points; +} diff --git a/packages/schema/src/domains/channel/ChannelRequestSchemas.ts b/packages/schema/src/domains/channel/ChannelRequestSchemas.ts index 31a2d8fa3..73cee0ad9 100644 --- a/packages/schema/src/domains/channel/ChannelRequestSchemas.ts +++ b/packages/schema/src/domains/channel/ChannelRequestSchemas.ts @@ -144,6 +144,10 @@ const ChannelUpdateCommon = ChannelCommonBase.extend({ .describe( `Voice region ID for the voice channel (${RTC_REGION_ID_MIN_LENGTH}-${RTC_REGION_ID_MAX_LENGTH} characters)`, ), + rtc_p2p: z + .boolean() + .optional() + .describe('Whether calls in the voice channel connect participants directly instead of through a voice server'), }); const ChannelCreateTextRequest = ChannelCreateCommon.extend({ type: createNamedLiteral(ChannelTypes.GUILD_TEXT, 'GUILD_TEXT', 'Channel type (text channel)'), diff --git a/packages/schema/src/domains/channel/ChannelSchemas.ts b/packages/schema/src/domains/channel/ChannelSchemas.ts index d4a4677fa..5ea8cd618 100644 --- a/packages/schema/src/domains/channel/ChannelSchemas.ts +++ b/packages/schema/src/domains/channel/ChannelSchemas.ts @@ -65,6 +65,10 @@ export const ChannelResponse = z.object({ 'The maximum active voice connections allowed per user in the voice channel', ), rtc_region: z.string().nullish().describe('The voice region ID for the voice channel'), + rtc_p2p: z + .boolean() + .optional() + .describe('Whether calls in the voice channel connect participants directly instead of through a voice server'), last_message_id: SnowflakeStringType.nullish().describe('The ID of the last message sent in this channel'), last_pin_timestamp: z.iso .datetime() @@ -160,6 +164,7 @@ export interface Channel { readonly user_limit?: number | null; readonly voice_connection_limit?: number | null; readonly rtc_region?: string | null; + readonly rtc_p2p?: boolean; readonly last_message_id?: string | null; readonly last_pin_timestamp?: string | null; readonly permission_overwrites?: ReadonlyArray; diff --git a/packages/schema/src/domains/experiment/ExperimentBucket.test.ts b/packages/schema/src/domains/experiment/ExperimentBucket.test.ts index d8b6fb2d7..95e985f1b 100644 --- a/packages/schema/src/domains/experiment/ExperimentBucket.test.ts +++ b/packages/schema/src/domains/experiment/ExperimentBucket.test.ts @@ -1,6 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket'; +import { + ExperimentRolloutCountryCodesSchema, + type ExperimentTargeting, + experimentBucket, + experimentRolloutCountryIncludes, +} from '@fluxer/schema/src/domains/experiment/ExperimentBucket'; import {describe, expect, test} from 'vitest'; const TARGETED_USER_ID = '1000000000000000001'; @@ -41,3 +46,45 @@ describe('experimentBucket', () => { expect(changed.length).toBeGreaterThan(userIds.length - 5); }); }); + +describe('ExperimentRolloutCountryCodesSchema', () => { + test('accepts unique uppercase alpha-2 codes', () => { + expect(ExperimentRolloutCountryCodesSchema.parse(['SE', 'BR'])).toEqual(['SE', 'BR']); + expect(ExperimentRolloutCountryCodesSchema.parse([])).toEqual([]); + }); + + test.each([ + {name: 'lowercase codes', value: ['se']}, + {name: 'alpha-3 codes', value: ['SWE']}, + {name: 'duplicates', value: ['SE', 'SE']}, + { + name: 'more than 250 codes', + value: Array.from( + {length: 251}, + (_, index) => `${String.fromCharCode(65 + Math.floor(index / 26))}${String.fromCharCode(65 + (index % 26))}`, + ), + }, + ])('rejects $name', ({value}) => { + expect(ExperimentRolloutCountryCodesSchema.safeParse(value).success).toBe(false); + }); +}); + +describe('experimentRolloutCountryIncludes', () => { + const targeting = (countryCode: string | null): ExperimentTargeting => ({ + memberGuildIds: new Set(), + premium: false, + countryCode, + }); + + test('matches every country while the list is empty', () => { + expect(experimentRolloutCountryIncludes({rollout_country_codes: []}, targeting('SE'))).toBe(true); + expect(experimentRolloutCountryIncludes({rollout_country_codes: []}, targeting(null))).toBe(true); + }); + + test('matches only listed countries and never an unknown country', () => { + const rules = {rollout_country_codes: ['SE', 'NO']}; + expect(experimentRolloutCountryIncludes(rules, targeting('NO'))).toBe(true); + expect(experimentRolloutCountryIncludes(rules, targeting('BR'))).toBe(false); + expect(experimentRolloutCountryIncludes(rules, targeting(null))).toBe(false); + }); +}); diff --git a/packages/schema/src/domains/experiment/ExperimentBucket.ts b/packages/schema/src/domains/experiment/ExperimentBucket.ts index 9eba7390a..07d2bfac1 100644 --- a/packages/schema/src/domains/experiment/ExperimentBucket.ts +++ b/packages/schema/src/domains/experiment/ExperimentBucket.ts @@ -1,9 +1,19 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import {z} from 'zod'; + const FNV_OFFSET_BASIS_32 = 0x811c9dc5; const FNV_PRIME_32 = 0x01000193; export const EXPERIMENT_BUCKET_RESOLUTION = 10000; +const EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES = 250; + +export const ExperimentRolloutCountryCodesSchema = z + .array(z.string().regex(/^[A-Z]{2}$/u)) + .max(EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES) + .refine((countryCodes) => new Set(countryCodes).size === countryCodes.length, { + message: 'Country codes must be unique', + }); export function experimentBucket(userId: string, salt: string): number { let hash = FNV_OFFSET_BASIS_32; @@ -18,6 +28,7 @@ export function experimentBucket(userId: string, salt: string): number { export interface ExperimentTargeting { readonly memberGuildIds: ReadonlySet; readonly premium: boolean; + readonly countryCode: string | null; } interface ExperimentAudienceRules { @@ -29,3 +40,15 @@ export function experimentAudienceIncludes(rules: ExperimentAudienceRules, targe if (rules.include_premium_users && targeting.premium) return true; return rules.included_guild_ids.some((guildId) => targeting.memberGuildIds.has(guildId)); } + +interface ExperimentRolloutCountryRules { + readonly rollout_country_codes: ReadonlyArray; +} + +export function experimentRolloutCountryIncludes( + rules: ExperimentRolloutCountryRules, + targeting: ExperimentTargeting, +): boolean { + if (rules.rollout_country_codes.length === 0) return true; + return targeting.countryCode !== null && rules.rollout_country_codes.includes(targeting.countryCode); +} diff --git a/packages/schema/src/domains/experiment/ExperimentSchemas.ts b/packages/schema/src/domains/experiment/ExperimentSchemas.ts index 3bee3f602..72e25215c 100644 --- a/packages/schema/src/domains/experiment/ExperimentSchemas.ts +++ b/packages/schema/src/domains/experiment/ExperimentSchemas.ts @@ -9,6 +9,7 @@ import { INERT_PLUTONIUM_PAGE_ASSIGNMENT, PlutoniumPageAssignmentResponse, } from '@fluxer/schema/src/domains/admin/PlutoniumPageSchemas'; +import {INERT_VOICE_P2P_ASSIGNMENT, VoiceP2pAssignmentResponse} from '@fluxer/schema/src/domains/admin/VoiceP2pSchemas'; import {z} from 'zod'; export const EXPERIMENT_MIN_POLL_INTERVAL_SECONDS = 60; @@ -49,6 +50,7 @@ const ExperimentAssignmentsSchema = z.object({ domain_migration: DomainMigrationAssignmentResponse.optional(), channel_threads: ChannelThreadsAssignmentResponse.optional(), plutonium_page: PlutoniumPageAssignmentResponse.optional(), + voice_p2p: VoiceP2pAssignmentResponse.optional(), }); export const ExperimentAssignmentsResponse = z.object({ @@ -80,3 +82,7 @@ export function readChannelThreadsAssignment( export function readPlutoniumPageAssignment(response: ExperimentAssignmentsResponse): PlutoniumPageAssignmentResponse { return response.assignments.plutonium_page ?? INERT_PLUTONIUM_PAGE_ASSIGNMENT; } + +export function readVoiceP2pAssignment(response: ExperimentAssignmentsResponse): VoiceP2pAssignmentResponse { + return response.assignments.voice_p2p ?? INERT_VOICE_P2P_ASSIGNMENT; +} diff --git a/packages/schema/src/domains/gateway/GatewaySchemas.ts b/packages/schema/src/domains/gateway/GatewaySchemas.ts index ccd19123f..4a3694ca0 100644 --- a/packages/schema/src/domains/gateway/GatewaySchemas.ts +++ b/packages/schema/src/domains/gateway/GatewaySchemas.ts @@ -25,6 +25,12 @@ export const VoiceStateResponse = z.object({ .optional() .describe('The stream keys the user is currently viewing'), version: Int32Type.optional().describe('The voice state version for ordering updates'), + p2p: z + .boolean() + .optional() + .describe( + 'Whether this connection exchanges media peer-to-peer. A channel or call is peer-to-peer iff it has at least one voice state and every voice state in it has p2p=true.', + ), e2ee_capable: z .boolean() .optional() diff --git a/packages/schema/src/domains/rpc/RpcSchemas.ts b/packages/schema/src/domains/rpc/RpcSchemas.ts index 353306865..b2866038c 100644 --- a/packages/schema/src/domains/rpc/RpcSchemas.ts +++ b/packages/schema/src/domains/rpc/RpcSchemas.ts @@ -1,6 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {RTC_REGION_ID_MAX_LENGTH, RTC_REGION_ID_MIN_LENGTH} from '@fluxer/constants/src/LimitConstants'; +import { + RTC_REGION_ID_MAX_LENGTH, + RTC_REGION_ID_MIN_LENGTH, + VOICE_P2P_MAX_PARTICIPANTS, +} from '@fluxer/constants/src/LimitConstants'; import {FORUM_UNREAD_COUNT_CAP, FORUM_UNREADS_MAX_THREADS} from '@fluxer/constants/src/ThreadConstants'; import {ChannelThreadsConfigResponse} from '@fluxer/schema/src/domains/admin/ChannelThreadsSchemas'; import {GatewayRolloutConfigResponse} from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas'; @@ -156,6 +160,20 @@ export const RpcRequest = z.discriminatedUnion('type', [ can_stream: z.boolean().optional().describe('Whether the user can stream in the channel'), can_video: z.boolean().optional().describe('Whether the user can use video in the channel'), token_nonce: createStringType(1, 64).optional().describe('Token nonce for replay prevention'), + p2p: z.boolean().optional().describe('Whether the gateway asks for a peer-to-peer grant for this connection'), + p2p_initiator: z.boolean().optional().describe('Whether this connection would start the peer-to-peer call'), + p2p_participant_count: z + .number() + .int() + .min(1) + .max(VOICE_P2P_MAX_PARTICIPANTS) + .optional() + .describe('Peer-to-peer participants in the channel or call once this connection joins'), + country_code: z + .string() + .regex(/^[A-Z]{2}$/u) + .optional() + .describe('ISO 3166-1 alpha-2 country code of the session, omitted when unknown'), }), z.object({ type: z.literal('kick_temporary_member').describe('Request type for kicking temporary guild members'), @@ -365,6 +383,10 @@ export const RpcResponseSessionData = z.object({ guilds: z.array(GuildReadyResponse).describe('Guilds the user is a member of'), pinned_dms: z.array(SnowflakeStringType).describe('IDs of pinned DM channels'), country_code: z.string().describe('Two-letter country code from IP geolocation'), + geoip_country_code: z + .string() + .nullable() + .describe('Two-letter country code from IP geolocation, null when the lookup found none'), latitude: createStringType(1, 32).optional().describe('Latitude from IP geolocation'), longitude: createStringType(1, 32).optional().describe('Longitude from IP geolocation'), rtc_regions: z.array(RtcRegionResponse).describe('Available voice server regions'), @@ -481,14 +503,35 @@ export const RpcResponse = z.discriminatedUnion('type', [ z.object({ type: z.literal('voice_get_token').describe('Response type for voice connection token'), data: z - .object({ - token: z.string().describe('Voice server authentication token'), - endpoint: z.string().describe('Voice server endpoint URL'), - connectionId: z.string().describe('Unique connection identifier'), - tokenNonce: z.string().describe('Token nonce for webhook confirmation'), - regionId: createStringType(1, 64).optional().describe('Voice region selected for the connection'), - serverId: createStringType(1, 128).optional().describe('Voice server selected for the connection'), - }) + .union([ + z.object({ + token: z.string().describe('Voice server authentication token'), + endpoint: z.string().describe('Voice server endpoint URL'), + connectionId: z.string().describe('Unique connection identifier'), + tokenNonce: z.string().describe('Token nonce for webhook confirmation'), + regionId: createStringType(1, 64).optional().describe('Voice region selected for the connection'), + serverId: createStringType(1, 128).optional().describe('Voice server selected for the connection'), + }), + z.object({ + p2p: z.literal(true).describe('Marks a peer-to-peer grant'), + connectionId: z.string().describe('Unique connection identifier'), + iceServers: z + .array(z.object({urls: z.array(z.string()).describe('STUN URLs of the server')})) + .describe('ICE servers the connection uses for its peer connections'), + maxParticipants: z + .number() + .int() + .min(1) + .max(VOICE_P2P_MAX_PARTICIPANTS) + .describe('Configured peer-to-peer participant cap the call enforces on this join'), + }), + z.object({ + p2p: z.literal(false).describe('Marks a declined peer-to-peer request'), + declineReason: z + .literal('channel_full') + .describe('The join would take the call past the configured peer-to-peer participant cap'), + }), + ]) .describe('Voice connection credentials'), }), z.object({ diff --git a/packages/schema/src/domains/tests/RequestTransportSchemas.test.ts b/packages/schema/src/domains/tests/RequestTransportSchemas.test.ts index 561e4dee9..73147790d 100644 --- a/packages/schema/src/domains/tests/RequestTransportSchemas.test.ts +++ b/packages/schema/src/domains/tests/RequestTransportSchemas.test.ts @@ -116,7 +116,14 @@ describe('channel update request body', () => { }); it('preserves guild-only fields in the documented body', () => { - const input = {topic: ' Topic ', parent_id: '123', nsfw_override: false, bitrate: 64000, rtc_region: null}; + const input = { + topic: ' Topic ', + parent_id: '123', + nsfw_override: false, + bitrate: 64000, + rtc_region: null, + rtc_p2p: true, + }; expect(ChannelUpdateRequestBody.parse(input)).toEqual({...input, topic: 'Topic', parent_id: 123n}); }); diff --git a/packages/schema/src/domains/voice/VoiceP2pConnectionReportSchemas.ts b/packages/schema/src/domains/voice/VoiceP2pConnectionReportSchemas.ts new file mode 100644 index 000000000..823737e71 --- /dev/null +++ b/packages/schema/src/domains/voice/VoiceP2pConnectionReportSchemas.ts @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {VOICE_P2P_MAX_PARTICIPANTS} from '@fluxer/constants/src/LimitConstants'; +import {createNamedStringLiteralUnion, Int32Type, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives'; +import {z} from 'zod'; + +const VOICE_P2P_CONNECTION_REPORTS_MAX = 8; + +const VoiceP2pCandidateType = createNamedStringLiteralUnion( + [ + ['host', 'host', 'A local interface address'], + ['srflx', 'srflx', 'A public address a STUN server reflected'], + ['prflx', 'prflx', 'A public address learned from a peer during checks'], + ['relay', 'relay', 'A relayed address'], + ], + 'ICE candidate type of one end of the selected candidate pair', +); + +const VoiceP2pConnectionReport = z.object({ + channel_id: SnowflakeType.describe('The voice channel or private channel of the call'), + guild_id: SnowflakeType.nullable().describe('The guild of the voice channel, null for a call'), + participant_count: z + .number() + .int() + .min(2) + .max(VOICE_P2P_MAX_PARTICIPANTS) + .describe('Participants in the call when the peer connection settled'), + outcome: createNamedStringLiteralUnion( + [ + ['connected', 'connected', 'The peer connection reached connected'], + ['failed', 'failed', 'The peer connection failed'], + ], + 'How the peer connection settled', + ), + local_candidate_type: VoiceP2pCandidateType.nullable().describe('Local candidate type, null without a selected pair'), + remote_candidate_type: VoiceP2pCandidateType.nullable().describe( + 'Remote candidate type, null without a selected pair', + ), + ip_family: createNamedStringLiteralUnion( + [ + ['ipv4', 'ipv4', 'IPv4'], + ['ipv6', 'ipv6', 'IPv6'], + ], + 'Address family of the selected pair', + ) + .nullable() + .describe('Address family of the selected pair, null without one'), + protocol: createNamedStringLiteralUnion( + [ + ['udp', 'udp', 'UDP'], + ['tcp', 'tcp', 'TCP'], + ], + 'Transport protocol of the selected pair', + ) + .nullable() + .describe('Transport protocol of the selected pair, null without one'), + setup_ms: Int32Type.nullable().describe('Milliseconds from peer connection creation to first connected'), + ice_restarted: z.boolean().describe('Whether the peer connection ran an ICE restart'), +}); + +export const VoiceP2pConnectionReportsRequest = z.object({ + reports: z + .array(VoiceP2pConnectionReport) + .min(1) + .max(VOICE_P2P_CONNECTION_REPORTS_MAX) + .describe('One report for each remote peer that settled'), +}); diff --git a/packages/schema/src/gen/fluxer/user/preferences/v1/preferences_pb.ts b/packages/schema/src/gen/fluxer/user/preferences/v1/preferences_pb.ts index e9b8cce35..a2bfc20d7 100644 --- a/packages/schema/src/gen/fluxer/user/preferences/v1/preferences_pb.ts +++ b/packages/schema/src/gen/fluxer/user/preferences/v1/preferences_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file fluxer/user/preferences/v1/preferences.proto. */ export const file_fluxer_user_preferences_v1_preferences: GenFile = /*@__PURE__*/ - fileDesc("CixmbHV4ZXIvdXNlci9wcmVmZXJlbmNlcy92MS9wcmVmZXJlbmNlcy5wcm90bxIaZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEiwRYKEVN5bmNlZFByZWZlcmVuY2VzEkgKDWFjY2Vzc2liaWxpdHkYASABKAsyMS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5U2V0dGluZ3MSUwoXYWNjZXNzaWJpbGl0eV9vdmVycmlkZXMYAiABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5T3ZlcnJpZGVzEksKD3RleHR1YWxfcHJldmlldxgDIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSQgoMZW1vamlfcGlja2VyGBQgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlQaWNrZXJTdGF0ZRJGCg5zdGlja2VyX3BpY2tlchgVIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN0aWNrZXJQaWNrZXJTdGF0ZRJCCgxtZW1lc19waWNrZXIYFiABKAsyLC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW1lc1BpY2tlclN0YXRlEjUKBWVtb2ppGBcgASgLMiYuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlTdGF0ZRJUChRlbW9qaV9zdGlja2VyX2xheW91dBgYIAEoCzI2LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkVtb2ppU3RpY2tlckxheW91dFNldHRpbmdzEkYKDWZhdm9yaXRlX2dpZnMYGSABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5GYXZvcml0ZUdpZlNldHRpbmdzEj0KCWZhdm9yaXRlcxgoIAEoCzIqLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkZhdm9yaXRlc1N0YXRlEksKD3JlY2VudF9tZW50aW9ucxgpIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlJlY2VudE1lbnRpb25zU2V0dGluZ3MSPwoHc2lkZWJhchgqIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlNpZGViYXJQcmVmZXJlbmNlcxJACgttZW1iZXJfbGlzdBgrIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk1lbWJlckxpc3RTdGF0ZRJICg91bnJlYWRfY2hhbm5lbHMYLCABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5VbnJlYWRDaGFubmVsc1N0YXRlEkoKEG1lbnRpb25fZnJlY2VuY3kYLSABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW50aW9uRnJlY2VuY3lTdGF0ZRI9CgduYWdiYXJzGDwgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2FscxJHChFkaXNtaXNzZWRfdXBzZWxscxg9IAEoCzIsLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkRpc21pc3NlZFVwc2VsbHMSTgoVZ3VpbGRfbnNmd19hZ3JlZW1lbnRzGD4gASgLMi8uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGROc2Z3QWdyZWVtZW50cxI8Cgl3aGF0c19uZXcYPyABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5XaGF0c05ld1N0YXRlEj8KB3ByaXZhY3kYUCABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Qcml2YWN5UHJlZmVyZW5jZXMSUAoUbG9jYWxfc3BhbV9vdmVycmlkZXMYUSABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Mb2NhbFVzZXJTcGFtT3ZlcnJpZGVzEhUKDXNhbml0aXplX3VybHMYUiABKAgSOAoFc291bmQYZCABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Tb3VuZFNldHRpbmdzEkIKCnNwZWxsY2hlY2sYZSABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TcGVsbGNoZWNrU2V0dGluZ3MSSAoOc2VhcmNoX2VuZ2luZXMYZiABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TZWFyY2hFbmdpbmVTZXR0aW5ncxJPChFwZXJtaXNzaW9uX2xheW91dBhnIAEoCzI0LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlBlcm1pc3Npb25MYXlvdXRTZXR0aW5ncxJSChNndWlsZF9tZW1iZXJfbGF5b3V0GGggASgLMjUuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRNZW1iZXJMYXlvdXRTZXR0aW5ncxJLCg1ndWlsZF9mb2xkZXJzGGkgASgLMjQuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRGb2xkZXJFeHBhbmRlZFN0YXRlElAKFGhpZGRlbl9ndWlsZF9idXR0b25zGGogASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSGlkZGVuR3VpbGRMaXN0QnV0dG9ucxJPChNrZXlib2FyZF9tb2RlX2ludHJvGGsgASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuS2V5Ym9hcmRNb2RlSW50cm9TdGF0ZRJRChBpbnB1dF9tb25pdG9yaW5nGGwgASgLMjcuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSW5wdXRNb25pdG9yaW5nUHJvbXB0c1N0YXRlEkQKDXZvaWNlX3Byb21wdHMYbSABKAsyLS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Wb2ljZVByb21wdHNTdGF0ZRJACgtzdWRvX3Byb21wdBhuIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN1ZG9Qcm9tcHRTdGF0ZRI9CghrZXliaW5kcxhvIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLktleWJpbmRTZXR0aW5ncxJBCgpjaGF0X2lucHV0GHAgASgLMi0uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhdElucHV0U2V0dGluZ3MSKgodc2F2ZV9jYW1lcmFfdXBsb2Fkc190b19kZXZpY2UYcSABKAhIAIgBARJGChNkb3VibGVfdGFwX3JlYWN0aW9uGHIgASgLMikuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuUmVhY3Rpb25FbW9qaRJKChBjaGFubmVsX2ZyZWNlbmN5GHMgASgLMjAuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhbm5lbEZyZWNlbmN5U3RhdGUSUgoUYW5ub3VuY2VtZW50X3Byb21wdHMYdCABKAsyNC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Bbm5vdW5jZW1lbnRQcm9tcHRzU3RhdGUSRgoRZG91YmxlX3RhcF9hY3Rpb24YdSABKA4yKy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Eb3VibGVUYXBBY3Rpb25CIAoeX3NhdmVfY2FtZXJhX3VwbG9hZHNfdG9fZGV2aWNlIrABChJTcGVsbGNoZWNrU2V0dGluZ3MSFAoHZW5hYmxlZBgBIAEoCEgAiAEBEhEKCWxhbmd1YWdlcxgCIAMoCRIbChNwZXJzb25hbF9kaWN0aW9uYXJ5GAMgAygJEhgKC2F1dG9fZGV0ZWN0GAQgASgISAGIAQESEwoGZW5naW5lGAUgASgJSAKIAQFCCgoIX2VuYWJsZWRCDgoMX2F1dG9fZGV0ZWN0QgkKB19lbmdpbmUi5gEKFFNlYXJjaEVuZ2luZVNldHRpbmdzEiIKFXRleHRfc2VhcmNoX2VuZ2luZV9pZBgBIAEoCUgAiAEBEisKHnJldmVyc2VfaW1hZ2Vfc2VhcmNoX2VuZ2luZV9pZBgCIAEoCUgBiAEBEiQKF3RyYW5zbGF0aW9uX3Byb3ZpZGVyX2lkGAMgASgJSAKIAQFCGAoWX3RleHRfc2VhcmNoX2VuZ2luZV9pZEIhCh9fcmV2ZXJzZV9pbWFnZV9zZWFyY2hfZW5naW5lX2lkQhoKGF90cmFuc2xhdGlvbl9wcm92aWRlcl9pZCK1AQoSUHJpdmFjeVByZWZlcmVuY2VzEh8KF2Rpc2FibGVfc3RyZWFtX3ByZXZpZXdzGAEgASgIEhwKD3Nob3dfYWN0aXZlX25vdxgCIAEoCEgAiAEBEioKHXByZXVwbG9hZF9tZXNzYWdlX2F0dGFjaG1lbnRzGAMgASgISAGIAQFCEgoQX3Nob3dfYWN0aXZlX25vd0IgCh5fcHJldXBsb2FkX21lc3NhZ2VfYXR0YWNobWVudHMiUAoWTG9jYWxVc2VyU3BhbU92ZXJyaWRlcxIYChBzcGFtbWVyX3VzZXJfaWRzGAEgAygJEhwKFG5vdF9zcGFtbWVyX3VzZXJfaWRzGAIgAygJIisKFlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSEQoJd3JhcF90ZXh0GAEgASgIItwBChJTaWRlYmFyUHJlZmVyZW5jZXMSHAoUaW5saW5lX2Rtc19jb2xsYXBzZWQYASABKAgSLAofc2hvd19jb2xsYXBzZWRfdW5yZWFkX2Rtc19iYWRnZRgCIAEoCEgAiAEBEi8KInNob3dfaW5jb21pbmdfZnJpZW5kX3JlcXVlc3RfYmFkZ2UYAyABKAhIAYgBAUIiCiBfc2hvd19jb2xsYXBzZWRfdW5yZWFkX2Rtc19iYWRnZUIlCiNfc2hvd19pbmNvbWluZ19mcmllbmRfcmVxdWVzdF9iYWRnZSI9Cg9NZW1iZXJMaXN0U3RhdGUSGQoMbWVtYmVyc19vcGVuGAEgASgISACIAQFCDwoNX21lbWJlcnNfb3BlbiI0ChNVbnJlYWRDaGFubmVsc1N0YXRlEh0KFWNvbGxhcHNlZF9jaGFubmVsX2lkcxgBIAMoCSKqAQoWUmVjZW50TWVudGlvbnNTZXR0aW5ncxIdChBpbmNsdWRlX2V2ZXJ5b25lGAEgASgISACIAQESGgoNaW5jbHVkZV9yb2xlcxgCIAEoCEgBiAEBEhsKDmluY2x1ZGVfZ3VpbGRzGAMgASgISAKIAQFCEwoRX2luY2x1ZGVfZXZlcnlvbmVCEAoOX2luY2x1ZGVfcm9sZXNCEQoPX2luY2x1ZGVfZ3VpbGRzIv8BChRNZW50aW9uRnJlY2VuY3lTdGF0ZRJGCgZzY29wZXMYASADKAsyNi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW50aW9uRnJlY2VuY3lTdGF0ZS5TY29wZRo7CgVFbnRyeRIPCgd1c2VyX2lkGAEgASgJEg0KBWNvdW50GAIgASgNEhIKCmxhc3RfYXRfbXMYAyABKAMaYgoFU2NvcGUSEAoIZ3VpbGRfaWQYASABKAkSRwoHZW50cmllcxgCIAMoCzI2LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk1lbnRpb25GcmVjZW5jeVN0YXRlLkVudHJ5It0BCg5GYXZvcml0ZXNTdGF0ZRI9CghjaGFubmVscxgBIAMoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkZhdm9yaXRlQ2hhbm5lbBJACgpjYXRlZ29yaWVzGAIgAygLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRmF2b3JpdGVDYXRlZ29yeRIeChZjb2xsYXBzZWRfY2F0ZWdvcnlfaWRzGAMgAygJEhsKE2hpZGVfbXV0ZWRfY2hhbm5lbHMYBCABKAgSDQoFbXV0ZWQYBSABKAgikwEKD0Zhdm9yaXRlQ2hhbm5lbBISCgpjaGFubmVsX2lkGAEgASgJEhAKCGd1aWxkX2lkGAIgASgJEhYKCXBhcmVudF9pZBgDIAEoCUgAiAEBEhAKCHBvc2l0aW9uGAQgASgFEhUKCG5pY2tuYW1lGAUgASgJSAGIAQFCDAoKX3BhcmVudF9pZEILCglfbmlja25hbWUiPgoQRmF2b3JpdGVDYXRlZ29yeRIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhAKCHBvc2l0aW9uGAMgASgFIuMIChBOYWdiYXJEaXNtaXNzYWxzEhMKC2lvc19pbnN0YWxsGAEgASgIEhMKC3B3YV9pbnN0YWxsGAIgASgIEhkKEXB1c2hfbm90aWZpY2F0aW9uGAMgASgIEhwKFGRlc2t0b3Bfbm90aWZpY2F0aW9uGAQgASgIEhwKFHByZW1pdW1fZ3JhY2VfcGVyaW9kGAUgASgIEhcKD3ByZW1pdW1fZXhwaXJlZBgGIAEoCBIaChJwcmVtaXVtX29uYm9hcmRpbmcYByABKAgSHgoWcHJlbWl1bV90cmlhbF9leHBpcmluZxgIIAEoCBIWCg5naWZ0X2ludmVudG9yeRgJIAEoCBIYChBkZXNrdG9wX2Rvd25sb2FkGAogASgIEhwKFGd1aWxkX21lbWJlcnNoaXBfY3RhGAsgASgIEhUKDXZpc2lvbmFyeV9tZmEYDCABKAgSZAoVcGVuZGluZ19idWxrX2RlbGV0aW9uGA8gAygLMkUuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2Fscy5QZW5kaW5nQnVsa0RlbGV0aW9uRW50cnkSWwoQaW52aXRlc19kaXNhYmxlZBgQIAMoCzJBLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk5hZ2JhckRpc21pc3NhbHMuSW52aXRlc0Rpc2FibGVkRW50cnkSZAoVZ3VpbGRfbWZhX3JlcXVpcmVtZW50GBEgAygLMkUuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2Fscy5HdWlsZE1mYVJlcXVpcmVtZW50RW50cnkSXwoScHJpY2VfYW5ub3VuY2VtZW50GBIgAygLMkMuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2Fscy5QcmljZUFubm91bmNlbWVudEVudHJ5El8KE2xlZ2FjeV9wcmljZV9vcHRfaW4YEyADKAsyQi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5OYWdiYXJEaXNtaXNzYWxzLkxlZ2FjeVByaWNlT3B0SW5FbnRyeRo6ChhQZW5kaW5nQnVsa0RlbGV0aW9uRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgIOgI4ARo2ChRJbnZpdGVzRGlzYWJsZWRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAg6AjgBGjoKGEd1aWxkTWZhUmVxdWlyZW1lbnRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAg6AjgBGjgKFlByaWNlQW5ub3VuY2VtZW50RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgIOgI4ARo3ChVMZWdhY3lQcmljZU9wdEluRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgIOgI4AUoECA4QDyIqChBEaXNtaXNzZWRVcHNlbGxzEhYKDnBpY2tlcl9wcmVtaXVtGAEgASgIImgKE0d1aWxkTnNmd0FncmVlbWVudHMSGgoSYWdyZWVkX2NoYW5uZWxfaWRzGAEgAygJEhgKEGFncmVlZF9ndWlsZF9pZHMYAiADKAkSGwoTYWdyZWVkX2NhdGVnb3J5X2lkcxgDIAMoCSJRCg1XaGF0c05ld1N0YXRlEiQKF2xhc3RfZGlzbWlzc2VkX2VudHJ5X2lkGAEgASgJSACIAQFCGgoYX2xhc3RfZGlzbWlzc2VkX2VudHJ5X2lkIpoBChhQZXJtaXNzaW9uTGF5b3V0U2V0dGluZ3MSQAoGbGF5b3V0GAEgASgOMjAuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuUGVybWlzc2lvbkxheW91dE1vZGUSPAoEZ3JpZBgCIAEoDjIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlBlcm1pc3Npb25HcmlkTW9kZSJaChlHdWlsZE1lbWJlckxheW91dFNldHRpbmdzEj0KBG1vZGUYASABKA4yLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5HdWlsZE1lbWJlclZpZXdNb2RlIjcKGEd1aWxkRm9sZGVyRXhwYW5kZWRTdGF0ZRIbChNleHBhbmRlZF9mb2xkZXJfaWRzGAEgAygGIkYKFkhpZGRlbkd1aWxkTGlzdEJ1dHRvbnMSFwoPZG93bmxvYWRfYnV0dG9uGAEgASgIEhMKC2hlbHBfYnV0dG9uGAIgASgIIiYKFktleWJvYXJkTW9kZUludHJvU3RhdGUSDAoEc2VlbhgBIAEoCCIvChtJbnB1dE1vbml0b3JpbmdQcm9tcHRzU3RhdGUSEAoIc2Vlbl9jdGEYASABKAgiZAoRVm9pY2VQcm9tcHRzU3RhdGUSJAocc2tpcF9oaWRlX293bl9jYW1lcmFfY29uZmlybRgBIAEoCBIpCiFza2lwX2hpZGVfb3duX3NjcmVlbnNoYXJlX2NvbmZpcm0YAiABKAgiNgoYQW5ub3VuY2VtZW50UHJvbXB0c1N0YXRlEhoKEmhpZGVfcHVibGlzaF9udWRnZRgBIAEoCCJ0Cg9TdWRvUHJvbXB0U3RhdGUSSAoUbGFzdF91c2VkX21mYV9tZXRob2QYASABKA4yJS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZmFNZXRob2RIAIgBAUIXChVfbGFzdF91c2VkX21mYV9tZXRob2QiugEKD0tleWJpbmRTZXR0aW5ncxJCCg9jdXN0b21fa2V5YmluZHMYASADKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5DdXN0b21LZXliaW5kEhUKDXRyYW5zbWl0X21vZGUYAiABKAkSKgodcHVzaF90b190YWxrX3JlbGVhc2VfZGVsYXlfbXMYAyABKA1IAIgBAUIgCh5fcHVzaF90b190YWxrX3JlbGVhc2VfZGVsYXlfbXMihQEKDUN1c3RvbUtleWJpbmQSCgoCaWQYASABKAkSEwoGYWN0aW9uGAIgASgJSACIAQESNwoFY29tYm8YAyABKAsyKC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5LZXliaW5kQ29tYm8SDwoHZW5hYmxlZBgEIAEoCEIJCgdfYWN0aW9uIs4CCgxLZXliaW5kQ29tYm8SCwoDa2V5GAEgASgJEhEKBGNvZGUYAiABKAlIAIgBARIUCgxjdHJsX29yX21ldGEYAyABKAgSDAoEY3RybBgEIAEoCBILCgNhbHQYBSABKAgSDQoFc2hpZnQYBiABKAgSDAoEbWV0YRgHIAEoCBITCgZnbG9iYWwYCCABKAhIAYgBARIUCgdlbmFibGVkGAkgASgISAKIAQESFQoNbW9kaWZpZXJfb25seRgKIAEoCBISCgpib3RoX3NpZGVzGAsgASgIEhkKDG1vdXNlX2J1dHRvbhgMIAEoDUgDiAEBEhsKDmdhbWVwYWRfYnV0dG9uGA0gASgNSASIAQFCBwoFX2NvZGVCCQoHX2dsb2JhbEIKCghfZW5hYmxlZEIPCg1fbW91c2VfYnV0dG9uQhEKD19nYW1lcGFkX2J1dHRvbiJCChRDaGFubmVsRnJlY2VuY3lFbnRyeRISCgp0b3RhbF91c2VzGAEgASgNEhYKDnJlY2VudF91c2VzX21zGAIgAygDIsIBChRDaGFubmVsRnJlY2VuY3lTdGF0ZRJKCgV1c2FnZRgBIAMoCzI7LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkNoYW5uZWxGcmVjZW5jeVN0YXRlLlVzYWdlRW50cnkaXgoKVXNhZ2VFbnRyeRILCgNrZXkYASABKAkSPwoFdmFsdWUYAiABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5DaGFubmVsRnJlY2VuY3lFbnRyeToCOAEiSQoRQ2hhdElucHV0U2V0dGluZ3MSHgoRY29udmVydF9lbW90aWNvbnMYASABKAhIAIgBAUIUChJfY29udmVydF9lbW90aWNvbnMiNQoNUmVhY3Rpb25FbW9qaRIPCgJpZBgBIAEoCUgAiAEBEgwKBG5hbWUYAiABKAlCBQoDX2lkKoIBChRQZXJtaXNzaW9uTGF5b3V0TW9kZRImCiJQRVJNSVNTSU9OX0xBWU9VVF9NT0RFX1VOU1BFQ0lGSUVEEAASIAocUEVSTUlTU0lPTl9MQVlPVVRfTU9ERV9DT01GWRABEiAKHFBFUk1JU1NJT05fTEFZT1VUX01PREVfREVOU0UQAip6ChJQZXJtaXNzaW9uR3JpZE1vZGUSJAogUEVSTUlTU0lPTl9HUklEX01PREVfVU5TUEVDSUZJRUQQABIfChtQRVJNSVNTSU9OX0dSSURfTU9ERV9TSU5HTEUQARIdChlQRVJNSVNTSU9OX0dSSURfTU9ERV9HUklEEAIqgAEKE0d1aWxkTWVtYmVyVmlld01vZGUSJgoiR1VJTERfTUVNQkVSX1ZJRVdfTU9ERV9VTlNQRUNJRklFRBAAEiAKHEdVSUxEX01FTUJFUl9WSUVXX01PREVfVEFCTEUQARIfChtHVUlMRF9NRU1CRVJfVklFV19NT0RFX0dSSUQQAipVCglNZmFNZXRob2QSGgoWTUZBX01FVEhPRF9VTlNQRUNJRklFRBAAEhMKD01GQV9NRVRIT0RfVE9UUBABEhcKE01GQV9NRVRIT0RfV0VCQVVUSE4QAyqJAQoPRG91YmxlVGFwQWN0aW9uEiEKHURPVUJMRV9UQVBfQUNUSU9OX1VOU1BFQ0lGSUVEEAASGwoXRE9VQkxFX1RBUF9BQ1RJT05fUkVBQ1QQARIaChZET1VCTEVfVEFQX0FDVElPTl9FRElUEAISGgoWRE9VQkxFX1RBUF9BQ1RJT05fTk9ORRADYgZwcm90bzM", [file_fluxer_user_preferences_v1_accessibility, file_fluxer_user_preferences_v1_pickers]); + fileDesc("CixmbHV4ZXIvdXNlci9wcmVmZXJlbmNlcy92MS9wcmVmZXJlbmNlcy5wcm90bxIaZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEiwRYKEVN5bmNlZFByZWZlcmVuY2VzEkgKDWFjY2Vzc2liaWxpdHkYASABKAsyMS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5U2V0dGluZ3MSUwoXYWNjZXNzaWJpbGl0eV9vdmVycmlkZXMYAiABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5T3ZlcnJpZGVzEksKD3RleHR1YWxfcHJldmlldxgDIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSQgoMZW1vamlfcGlja2VyGBQgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlQaWNrZXJTdGF0ZRJGCg5zdGlja2VyX3BpY2tlchgVIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN0aWNrZXJQaWNrZXJTdGF0ZRJCCgxtZW1lc19waWNrZXIYFiABKAsyLC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW1lc1BpY2tlclN0YXRlEjUKBWVtb2ppGBcgASgLMiYuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlTdGF0ZRJUChRlbW9qaV9zdGlja2VyX2xheW91dBgYIAEoCzI2LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkVtb2ppU3RpY2tlckxheW91dFNldHRpbmdzEkYKDWZhdm9yaXRlX2dpZnMYGSABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5GYXZvcml0ZUdpZlNldHRpbmdzEj0KCWZhdm9yaXRlcxgoIAEoCzIqLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkZhdm9yaXRlc1N0YXRlEksKD3JlY2VudF9tZW50aW9ucxgpIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlJlY2VudE1lbnRpb25zU2V0dGluZ3MSPwoHc2lkZWJhchgqIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlNpZGViYXJQcmVmZXJlbmNlcxJACgttZW1iZXJfbGlzdBgrIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk1lbWJlckxpc3RTdGF0ZRJICg91bnJlYWRfY2hhbm5lbHMYLCABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5VbnJlYWRDaGFubmVsc1N0YXRlEkoKEG1lbnRpb25fZnJlY2VuY3kYLSABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW50aW9uRnJlY2VuY3lTdGF0ZRI9CgduYWdiYXJzGDwgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2FscxJHChFkaXNtaXNzZWRfdXBzZWxscxg9IAEoCzIsLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkRpc21pc3NlZFVwc2VsbHMSTgoVZ3VpbGRfbnNmd19hZ3JlZW1lbnRzGD4gASgLMi8uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGROc2Z3QWdyZWVtZW50cxI8Cgl3aGF0c19uZXcYPyABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5XaGF0c05ld1N0YXRlEj8KB3ByaXZhY3kYUCABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Qcml2YWN5UHJlZmVyZW5jZXMSUAoUbG9jYWxfc3BhbV9vdmVycmlkZXMYUSABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Mb2NhbFVzZXJTcGFtT3ZlcnJpZGVzEhUKDXNhbml0aXplX3VybHMYUiABKAgSOAoFc291bmQYZCABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Tb3VuZFNldHRpbmdzEkIKCnNwZWxsY2hlY2sYZSABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TcGVsbGNoZWNrU2V0dGluZ3MSSAoOc2VhcmNoX2VuZ2luZXMYZiABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TZWFyY2hFbmdpbmVTZXR0aW5ncxJPChFwZXJtaXNzaW9uX2xheW91dBhnIAEoCzI0LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlBlcm1pc3Npb25MYXlvdXRTZXR0aW5ncxJSChNndWlsZF9tZW1iZXJfbGF5b3V0GGggASgLMjUuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRNZW1iZXJMYXlvdXRTZXR0aW5ncxJLCg1ndWlsZF9mb2xkZXJzGGkgASgLMjQuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRGb2xkZXJFeHBhbmRlZFN0YXRlElAKFGhpZGRlbl9ndWlsZF9idXR0b25zGGogASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSGlkZGVuR3VpbGRMaXN0QnV0dG9ucxJPChNrZXlib2FyZF9tb2RlX2ludHJvGGsgASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuS2V5Ym9hcmRNb2RlSW50cm9TdGF0ZRJRChBpbnB1dF9tb25pdG9yaW5nGGwgASgLMjcuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSW5wdXRNb25pdG9yaW5nUHJvbXB0c1N0YXRlEkQKDXZvaWNlX3Byb21wdHMYbSABKAsyLS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Wb2ljZVByb21wdHNTdGF0ZRJACgtzdWRvX3Byb21wdBhuIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN1ZG9Qcm9tcHRTdGF0ZRI9CghrZXliaW5kcxhvIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLktleWJpbmRTZXR0aW5ncxJBCgpjaGF0X2lucHV0GHAgASgLMi0uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhdElucHV0U2V0dGluZ3MSKgodc2F2ZV9jYW1lcmFfdXBsb2Fkc190b19kZXZpY2UYcSABKAhIAIgBARJGChNkb3VibGVfdGFwX3JlYWN0aW9uGHIgASgLMikuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuUmVhY3Rpb25FbW9qaRJKChBjaGFubmVsX2ZyZWNlbmN5GHMgASgLMjAuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhbm5lbEZyZWNlbmN5U3RhdGUSUgoUYW5ub3VuY2VtZW50X3Byb21wdHMYdCABKAsyNC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Bbm5vdW5jZW1lbnRQcm9tcHRzU3RhdGUSRgoRZG91YmxlX3RhcF9hY3Rpb24YdSABKA4yKy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Eb3VibGVUYXBBY3Rpb25CIAoeX3NhdmVfY2FtZXJhX3VwbG9hZHNfdG9fZGV2aWNlIrABChJTcGVsbGNoZWNrU2V0dGluZ3MSFAoHZW5hYmxlZBgBIAEoCEgAiAEBEhEKCWxhbmd1YWdlcxgCIAMoCRIbChNwZXJzb25hbF9kaWN0aW9uYXJ5GAMgAygJEhgKC2F1dG9fZGV0ZWN0GAQgASgISAGIAQESEwoGZW5naW5lGAUgASgJSAKIAQFCCgoIX2VuYWJsZWRCDgoMX2F1dG9fZGV0ZWN0QgkKB19lbmdpbmUi5gEKFFNlYXJjaEVuZ2luZVNldHRpbmdzEiIKFXRleHRfc2VhcmNoX2VuZ2luZV9pZBgBIAEoCUgAiAEBEisKHnJldmVyc2VfaW1hZ2Vfc2VhcmNoX2VuZ2luZV9pZBgCIAEoCUgBiAEBEiQKF3RyYW5zbGF0aW9uX3Byb3ZpZGVyX2lkGAMgASgJSAKIAQFCGAoWX3RleHRfc2VhcmNoX2VuZ2luZV9pZEIhCh9fcmV2ZXJzZV9pbWFnZV9zZWFyY2hfZW5naW5lX2lkQhoKGF90cmFuc2xhdGlvbl9wcm92aWRlcl9pZCK1AQoSUHJpdmFjeVByZWZlcmVuY2VzEh8KF2Rpc2FibGVfc3RyZWFtX3ByZXZpZXdzGAEgASgIEhwKD3Nob3dfYWN0aXZlX25vdxgCIAEoCEgAiAEBEioKHXByZXVwbG9hZF9tZXNzYWdlX2F0dGFjaG1lbnRzGAMgASgISAGIAQFCEgoQX3Nob3dfYWN0aXZlX25vd0IgCh5fcHJldXBsb2FkX21lc3NhZ2VfYXR0YWNobWVudHMiUAoWTG9jYWxVc2VyU3BhbU92ZXJyaWRlcxIYChBzcGFtbWVyX3VzZXJfaWRzGAEgAygJEhwKFG5vdF9zcGFtbWVyX3VzZXJfaWRzGAIgAygJIisKFlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSEQoJd3JhcF90ZXh0GAEgASgIItwBChJTaWRlYmFyUHJlZmVyZW5jZXMSHAoUaW5saW5lX2Rtc19jb2xsYXBzZWQYASABKAgSLAofc2hvd19jb2xsYXBzZWRfdW5yZWFkX2Rtc19iYWRnZRgCIAEoCEgAiAEBEi8KInNob3dfaW5jb21pbmdfZnJpZW5kX3JlcXVlc3RfYmFkZ2UYAyABKAhIAYgBAUIiCiBfc2hvd19jb2xsYXBzZWRfdW5yZWFkX2Rtc19iYWRnZUIlCiNfc2hvd19pbmNvbWluZ19mcmllbmRfcmVxdWVzdF9iYWRnZSI9Cg9NZW1iZXJMaXN0U3RhdGUSGQoMbWVtYmVyc19vcGVuGAEgASgISACIAQFCDwoNX21lbWJlcnNfb3BlbiI0ChNVbnJlYWRDaGFubmVsc1N0YXRlEh0KFWNvbGxhcHNlZF9jaGFubmVsX2lkcxgBIAMoCSKqAQoWUmVjZW50TWVudGlvbnNTZXR0aW5ncxIdChBpbmNsdWRlX2V2ZXJ5b25lGAEgASgISACIAQESGgoNaW5jbHVkZV9yb2xlcxgCIAEoCEgBiAEBEhsKDmluY2x1ZGVfZ3VpbGRzGAMgASgISAKIAQFCEwoRX2luY2x1ZGVfZXZlcnlvbmVCEAoOX2luY2x1ZGVfcm9sZXNCEQoPX2luY2x1ZGVfZ3VpbGRzIv8BChRNZW50aW9uRnJlY2VuY3lTdGF0ZRJGCgZzY29wZXMYASADKAsyNi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW50aW9uRnJlY2VuY3lTdGF0ZS5TY29wZRo7CgVFbnRyeRIPCgd1c2VyX2lkGAEgASgJEg0KBWNvdW50GAIgASgNEhIKCmxhc3RfYXRfbXMYAyABKAMaYgoFU2NvcGUSEAoIZ3VpbGRfaWQYASABKAkSRwoHZW50cmllcxgCIAMoCzI2LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk1lbnRpb25GcmVjZW5jeVN0YXRlLkVudHJ5It0BCg5GYXZvcml0ZXNTdGF0ZRI9CghjaGFubmVscxgBIAMoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkZhdm9yaXRlQ2hhbm5lbBJACgpjYXRlZ29yaWVzGAIgAygLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRmF2b3JpdGVDYXRlZ29yeRIeChZjb2xsYXBzZWRfY2F0ZWdvcnlfaWRzGAMgAygJEhsKE2hpZGVfbXV0ZWRfY2hhbm5lbHMYBCABKAgSDQoFbXV0ZWQYBSABKAgikwEKD0Zhdm9yaXRlQ2hhbm5lbBISCgpjaGFubmVsX2lkGAEgASgJEhAKCGd1aWxkX2lkGAIgASgJEhYKCXBhcmVudF9pZBgDIAEoCUgAiAEBEhAKCHBvc2l0aW9uGAQgASgFEhUKCG5pY2tuYW1lGAUgASgJSAGIAQFCDAoKX3BhcmVudF9pZEILCglfbmlja25hbWUiPgoQRmF2b3JpdGVDYXRlZ29yeRIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhAKCHBvc2l0aW9uGAMgASgFIuMIChBOYWdiYXJEaXNtaXNzYWxzEhMKC2lvc19pbnN0YWxsGAEgASgIEhMKC3B3YV9pbnN0YWxsGAIgASgIEhkKEXB1c2hfbm90aWZpY2F0aW9uGAMgASgIEhwKFGRlc2t0b3Bfbm90aWZpY2F0aW9uGAQgASgIEhwKFHByZW1pdW1fZ3JhY2VfcGVyaW9kGAUgASgIEhcKD3ByZW1pdW1fZXhwaXJlZBgGIAEoCBIaChJwcmVtaXVtX29uYm9hcmRpbmcYByABKAgSHgoWcHJlbWl1bV90cmlhbF9leHBpcmluZxgIIAEoCBIWCg5naWZ0X2ludmVudG9yeRgJIAEoCBIYChBkZXNrdG9wX2Rvd25sb2FkGAogASgIEhwKFGd1aWxkX21lbWJlcnNoaXBfY3RhGAsgASgIEhUKDXZpc2lvbmFyeV9tZmEYDCABKAgSZAoVcGVuZGluZ19idWxrX2RlbGV0aW9uGA8gAygLMkUuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2Fscy5QZW5kaW5nQnVsa0RlbGV0aW9uRW50cnkSWwoQaW52aXRlc19kaXNhYmxlZBgQIAMoCzJBLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk5hZ2JhckRpc21pc3NhbHMuSW52aXRlc0Rpc2FibGVkRW50cnkSZAoVZ3VpbGRfbWZhX3JlcXVpcmVtZW50GBEgAygLMkUuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2Fscy5HdWlsZE1mYVJlcXVpcmVtZW50RW50cnkSXwoScHJpY2VfYW5ub3VuY2VtZW50GBIgAygLMkMuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2Fscy5QcmljZUFubm91bmNlbWVudEVudHJ5El8KE2xlZ2FjeV9wcmljZV9vcHRfaW4YEyADKAsyQi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5OYWdiYXJEaXNtaXNzYWxzLkxlZ2FjeVByaWNlT3B0SW5FbnRyeRo6ChhQZW5kaW5nQnVsa0RlbGV0aW9uRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgIOgI4ARo2ChRJbnZpdGVzRGlzYWJsZWRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAg6AjgBGjoKGEd1aWxkTWZhUmVxdWlyZW1lbnRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAg6AjgBGjgKFlByaWNlQW5ub3VuY2VtZW50RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgIOgI4ARo3ChVMZWdhY3lQcmljZU9wdEluRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgIOgI4AUoECA4QDyIqChBEaXNtaXNzZWRVcHNlbGxzEhYKDnBpY2tlcl9wcmVtaXVtGAEgASgIImgKE0d1aWxkTnNmd0FncmVlbWVudHMSGgoSYWdyZWVkX2NoYW5uZWxfaWRzGAEgAygJEhgKEGFncmVlZF9ndWlsZF9pZHMYAiADKAkSGwoTYWdyZWVkX2NhdGVnb3J5X2lkcxgDIAMoCSJRCg1XaGF0c05ld1N0YXRlEiQKF2xhc3RfZGlzbWlzc2VkX2VudHJ5X2lkGAEgASgJSACIAQFCGgoYX2xhc3RfZGlzbWlzc2VkX2VudHJ5X2lkIpoBChhQZXJtaXNzaW9uTGF5b3V0U2V0dGluZ3MSQAoGbGF5b3V0GAEgASgOMjAuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuUGVybWlzc2lvbkxheW91dE1vZGUSPAoEZ3JpZBgCIAEoDjIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlBlcm1pc3Npb25HcmlkTW9kZSJaChlHdWlsZE1lbWJlckxheW91dFNldHRpbmdzEj0KBG1vZGUYASABKA4yLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5HdWlsZE1lbWJlclZpZXdNb2RlIjcKGEd1aWxkRm9sZGVyRXhwYW5kZWRTdGF0ZRIbChNleHBhbmRlZF9mb2xkZXJfaWRzGAEgAygGIkYKFkhpZGRlbkd1aWxkTGlzdEJ1dHRvbnMSFwoPZG93bmxvYWRfYnV0dG9uGAEgASgIEhMKC2hlbHBfYnV0dG9uGAIgASgIIiYKFktleWJvYXJkTW9kZUludHJvU3RhdGUSDAoEc2VlbhgBIAEoCCIvChtJbnB1dE1vbml0b3JpbmdQcm9tcHRzU3RhdGUSEAoIc2Vlbl9jdGEYASABKAgigwEKEVZvaWNlUHJvbXB0c1N0YXRlEiQKHHNraXBfaGlkZV9vd25fY2FtZXJhX2NvbmZpcm0YASABKAgSKQohc2tpcF9oaWRlX293bl9zY3JlZW5zaGFyZV9jb25maXJtGAIgASgIEh0KFXNraXBfcDJwX2pvaW5fY29uZmlybRgDIAEoCCI2ChhBbm5vdW5jZW1lbnRQcm9tcHRzU3RhdGUSGgoSaGlkZV9wdWJsaXNoX251ZGdlGAEgASgIInQKD1N1ZG9Qcm9tcHRTdGF0ZRJIChRsYXN0X3VzZWRfbWZhX21ldGhvZBgBIAEoDjIlLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk1mYU1ldGhvZEgAiAEBQhcKFV9sYXN0X3VzZWRfbWZhX21ldGhvZCK6AQoPS2V5YmluZFNldHRpbmdzEkIKD2N1c3RvbV9rZXliaW5kcxgBIAMoCzIpLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkN1c3RvbUtleWJpbmQSFQoNdHJhbnNtaXRfbW9kZRgCIAEoCRIqCh1wdXNoX3RvX3RhbGtfcmVsZWFzZV9kZWxheV9tcxgDIAEoDUgAiAEBQiAKHl9wdXNoX3RvX3RhbGtfcmVsZWFzZV9kZWxheV9tcyKFAQoNQ3VzdG9tS2V5YmluZBIKCgJpZBgBIAEoCRITCgZhY3Rpb24YAiABKAlIAIgBARI3CgVjb21ibxgDIAEoCzIoLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLktleWJpbmRDb21ibxIPCgdlbmFibGVkGAQgASgIQgkKB19hY3Rpb24izgIKDEtleWJpbmRDb21ibxILCgNrZXkYASABKAkSEQoEY29kZRgCIAEoCUgAiAEBEhQKDGN0cmxfb3JfbWV0YRgDIAEoCBIMCgRjdHJsGAQgASgIEgsKA2FsdBgFIAEoCBINCgVzaGlmdBgGIAEoCBIMCgRtZXRhGAcgASgIEhMKBmdsb2JhbBgIIAEoCEgBiAEBEhQKB2VuYWJsZWQYCSABKAhIAogBARIVCg1tb2RpZmllcl9vbmx5GAogASgIEhIKCmJvdGhfc2lkZXMYCyABKAgSGQoMbW91c2VfYnV0dG9uGAwgASgNSAOIAQESGwoOZ2FtZXBhZF9idXR0b24YDSABKA1IBIgBAUIHCgVfY29kZUIJCgdfZ2xvYmFsQgoKCF9lbmFibGVkQg8KDV9tb3VzZV9idXR0b25CEQoPX2dhbWVwYWRfYnV0dG9uIkIKFENoYW5uZWxGcmVjZW5jeUVudHJ5EhIKCnRvdGFsX3VzZXMYASABKA0SFgoOcmVjZW50X3VzZXNfbXMYAiADKAMiwgEKFENoYW5uZWxGcmVjZW5jeVN0YXRlEkoKBXVzYWdlGAEgAygLMjsuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhbm5lbEZyZWNlbmN5U3RhdGUuVXNhZ2VFbnRyeRpeCgpVc2FnZUVudHJ5EgsKA2tleRgBIAEoCRI/CgV2YWx1ZRgCIAEoCzIwLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkNoYW5uZWxGcmVjZW5jeUVudHJ5OgI4ASJJChFDaGF0SW5wdXRTZXR0aW5ncxIeChFjb252ZXJ0X2Vtb3RpY29ucxgBIAEoCEgAiAEBQhQKEl9jb252ZXJ0X2Vtb3RpY29ucyI1Cg1SZWFjdGlvbkVtb2ppEg8KAmlkGAEgASgJSACIAQESDAoEbmFtZRgCIAEoCUIFCgNfaWQqggEKFFBlcm1pc3Npb25MYXlvdXRNb2RlEiYKIlBFUk1JU1NJT05fTEFZT1VUX01PREVfVU5TUEVDSUZJRUQQABIgChxQRVJNSVNTSU9OX0xBWU9VVF9NT0RFX0NPTUZZEAESIAocUEVSTUlTU0lPTl9MQVlPVVRfTU9ERV9ERU5TRRACKnoKElBlcm1pc3Npb25HcmlkTW9kZRIkCiBQRVJNSVNTSU9OX0dSSURfTU9ERV9VTlNQRUNJRklFRBAAEh8KG1BFUk1JU1NJT05fR1JJRF9NT0RFX1NJTkdMRRABEh0KGVBFUk1JU1NJT05fR1JJRF9NT0RFX0dSSUQQAiqAAQoTR3VpbGRNZW1iZXJWaWV3TW9kZRImCiJHVUlMRF9NRU1CRVJfVklFV19NT0RFX1VOU1BFQ0lGSUVEEAASIAocR1VJTERfTUVNQkVSX1ZJRVdfTU9ERV9UQUJMRRABEh8KG0dVSUxEX01FTUJFUl9WSUVXX01PREVfR1JJRBACKlUKCU1mYU1ldGhvZBIaChZNRkFfTUVUSE9EX1VOU1BFQ0lGSUVEEAASEwoPTUZBX01FVEhPRF9UT1RQEAESFwoTTUZBX01FVEhPRF9XRUJBVVRIThADKokBCg9Eb3VibGVUYXBBY3Rpb24SIQodRE9VQkxFX1RBUF9BQ1RJT05fVU5TUEVDSUZJRUQQABIbChdET1VCTEVfVEFQX0FDVElPTl9SRUFDVBABEhoKFkRPVUJMRV9UQVBfQUNUSU9OX0VESVQQAhIaChZET1VCTEVfVEFQX0FDVElPTl9OT05FEANiBnByb3RvMw", [file_fluxer_user_preferences_v1_accessibility, file_fluxer_user_preferences_v1_pickers]); /** * @generated from message fluxer.user.preferences.v1.SyncedPreferences @@ -892,6 +892,11 @@ export type VoicePromptsState = Message<"fluxer.user.preferences.v1.VoicePrompts * @generated from field: bool skip_hide_own_screenshare_confirm = 2; */ skipHideOwnScreenshareConfirm: boolean; + + /** + * @generated from field: bool skip_p2p_join_confirm = 3; + */ + skipP2pJoinConfirm: boolean; }; /** diff --git a/tools/dev/cassandra_target_schema.json b/tools/dev/cassandra_target_schema.json index 6667f7ac0..f8e460263 100644 --- a/tools/dev/cassandra_target_schema.json +++ b/tools/dev/cassandra_target_schema.json @@ -3615,6 +3615,10 @@ { "name": "content_warning_text", "type": "text" + }, + { + "name": "rtc_p2p", + "type": "boolean" } ], "primary_key": "((channel_id, soft_deleted))", @@ -8910,6 +8914,81 @@ "primary_key": "((region_id), server_id)", "options": "" }, + { + "name": "voice_p2p_connection_reports", + "columns": [ + { + "name": "user_id", + "type": "bigint" + }, + { + "name": "report_id", + "type": "bigint" + }, + { + "name": "reported_at", + "type": "timestamp" + }, + { + "name": "channel_id", + "type": "bigint" + }, + { + "name": "guild_id", + "type": "bigint" + }, + { + "name": "participant_count", + "type": "int" + }, + { + "name": "outcome", + "type": "text" + }, + { + "name": "local_candidate_type", + "type": "text" + }, + { + "name": "remote_candidate_type", + "type": "text" + }, + { + "name": "ip_family", + "type": "text" + }, + { + "name": "protocol", + "type": "text" + }, + { + "name": "setup_ms", + "type": "int" + }, + { + "name": "ice_restarted", + "type": "boolean" + }, + { + "name": "country", + "type": "text" + }, + { + "name": "ip", + "type": "text" + }, + { + "name": "client_platform", + "type": "text" + }, + { + "name": "client_os", + "type": "text" + } + ], + "primary_key": "((user_id), report_id)", + "options": "CLUSTERING ORDER BY (report_id DESC) AND default_time_to_live = 15552000 AND gc_grace_seconds = 864000 AND compaction = {'class': 'TimeWindowCompactionStrategy', 'compaction_window_unit': 'DAYS', 'compaction_window_size': '7'}" + }, { "name": "webauthn_credential_lookup", "columns": [